Archived
feat: unify Docker entrypoint
This commit is contained in:
Executable
+137
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat >&2 <<'EOF'
|
||||
Usage: hostexec [--root] [--] <command...>
|
||||
|
||||
Run a command on the Docker host by starting a temporary privileged helper
|
||||
container through the mounted Docker socket and entering the host namespaces.
|
||||
Commands run as the current container UID/GID by default. Use --root to keep
|
||||
root privileges for administrative host commands.
|
||||
EOF
|
||||
}
|
||||
|
||||
run_as_root=false
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--root)
|
||||
run_as_root=true
|
||||
shift
|
||||
;;
|
||||
--help|-h)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$#" -eq 0 ]; then
|
||||
usage
|
||||
exit 64
|
||||
fi
|
||||
|
||||
hostexec_mode="${HOSTEXEC_MODE:-nsenter}"
|
||||
case "$hostexec_mode" in
|
||||
nsenter) ;;
|
||||
disabled|none)
|
||||
echo "hostexec: disabled for this Docker host profile" >&2
|
||||
echo "hostexec: on Docker Desktop for Mac, containers run inside a Linux VM and cannot enter native macOS namespaces" >&2
|
||||
exit 69
|
||||
;;
|
||||
*)
|
||||
echo "hostexec: unsupported HOSTEXEC_MODE: $hostexec_mode" >&2
|
||||
exit 64
|
||||
;;
|
||||
esac
|
||||
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
echo "hostexec: docker CLI not found in this container" >&2
|
||||
exit 127
|
||||
fi
|
||||
|
||||
docker_host="${DOCKER_HOST:-unix:///var/run/docker.sock}"
|
||||
if [[ "$docker_host" == unix://* ]]; then
|
||||
socket_path="${docker_host#unix://}"
|
||||
if [ ! -S "$socket_path" ]; then
|
||||
echo "hostexec: Docker socket is not accessible as a Unix socket at $socket_path" >&2
|
||||
exit 69
|
||||
fi
|
||||
fi
|
||||
|
||||
helper_image="${HOSTEXEC_IMAGE:-alpine:3.22}"
|
||||
target_uid="$(id -u)"
|
||||
target_gid="$(id -g)"
|
||||
tty_args=(--interactive)
|
||||
if [ -t 0 ] && [ -t 1 ]; then
|
||||
tty_args+=(--tty)
|
||||
fi
|
||||
|
||||
docker_args=(
|
||||
--rm
|
||||
"${tty_args[@]}"
|
||||
--pull=missing
|
||||
--privileged
|
||||
--security-opt label=disable
|
||||
--pid=host
|
||||
--network=host
|
||||
--volume /:/host:rw
|
||||
)
|
||||
|
||||
if [ "$run_as_root" = true ] || { [ "$target_uid" = 0 ] && [ "$target_gid" = 0 ]; }; then
|
||||
exec docker run \
|
||||
"${docker_args[@]}" \
|
||||
"$helper_image" \
|
||||
nsenter -t 1 -m -u -i -n -p -- "$@"
|
||||
fi
|
||||
|
||||
run_as_container_user='target_uid="${HOSTEXEC_TARGET_UID:?}"
|
||||
target_gid="${HOSTEXEC_TARGET_GID:?}"
|
||||
|
||||
target_user=""
|
||||
if command -v getent >/dev/null 2>&1; then
|
||||
passwd_entry="$(getent passwd "$target_uid" || true)"
|
||||
if [ -n "$passwd_entry" ]; then
|
||||
target_user="${passwd_entry%%:*}"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$target_user" ]; then
|
||||
if command -v runuser >/dev/null 2>&1; then
|
||||
exec runuser -u "$target_user" -- "$@"
|
||||
fi
|
||||
|
||||
if command -v su >/dev/null 2>&1; then
|
||||
exec su -s /bin/sh -c '\''exec "$@"'\'' -- "$target_user" hostexec-su "$@"
|
||||
fi
|
||||
fi
|
||||
|
||||
if command -v setpriv >/dev/null 2>&1; then
|
||||
if [ -n "$target_user" ]; then
|
||||
exec setpriv --reuid "$target_uid" --regid "$target_gid" --init-groups -- "$@"
|
||||
fi
|
||||
|
||||
exec setpriv --reuid "$target_uid" --regid "$target_gid" --clear-groups -- "$@"
|
||||
fi
|
||||
|
||||
if command -v nsenter >/dev/null 2>&1; then
|
||||
exec nsenter -t 1 -m -u -i -n -p -S "$target_uid" -G "$target_gid" -- "$@"
|
||||
fi
|
||||
|
||||
echo "hostexec: unable to switch to host uid:gid $target_uid:$target_gid" >&2
|
||||
exit 69
|
||||
'
|
||||
|
||||
exec docker run \
|
||||
"${docker_args[@]}" \
|
||||
--env HOSTEXEC_TARGET_UID="$target_uid" \
|
||||
--env HOSTEXEC_TARGET_GID="$target_gid" \
|
||||
"$helper_image" \
|
||||
nsenter -t 1 -m -u -i -n -p -- /bin/sh -c "$run_as_container_user" hostexec-user "$@"
|
||||
Executable
+313
@@ -0,0 +1,313 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
log() {
|
||||
printf '%s\n' "$*" >&2
|
||||
}
|
||||
|
||||
die() {
|
||||
log "pi-web Docker dev compose: $*"
|
||||
exit 1
|
||||
}
|
||||
|
||||
script_dir=$(unset CDPATH; cd "$(dirname "$0")" && pwd -P)
|
||||
repo_root=$(unset CDPATH; cd "$script_dir/../../.." && pwd -P)
|
||||
dev_config_file=$repo_root/.pi-web/docker-compose-dev.local.env
|
||||
legacy_dev_env_file=$repo_root/.pi-web/docker-compose-dev.env
|
||||
generated_env_file=$repo_root/.pi-web/docker-compose-dev.generated.env
|
||||
|
||||
# shellcheck source=../host-profile.sh
|
||||
# shellcheck disable=SC1091
|
||||
. "$repo_root/docker/internal/host-profile.sh"
|
||||
|
||||
strip_wrapping_quotes() {
|
||||
value=$1
|
||||
case "$value" in
|
||||
\"*\")
|
||||
case "$value" in
|
||||
*\") value=${value#\"}; value=${value%\"} ;;
|
||||
esac
|
||||
;;
|
||||
\'*\')
|
||||
case "$value" in
|
||||
*\') value=${value#\'}; value=${value%\'} ;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
printf '%s\n' "$value"
|
||||
}
|
||||
|
||||
env_file_value() {
|
||||
file=$1
|
||||
key=$2
|
||||
[ -f "$file" ] || return 1
|
||||
raw=$(awk -v key="$key" '
|
||||
function trim(value) {
|
||||
sub(/^[ \t]+/, "", value)
|
||||
sub(/[ \t\r]+$/, "", value)
|
||||
return value
|
||||
}
|
||||
/^[ \t]*(#|$)/ { next }
|
||||
{
|
||||
line = $0
|
||||
sub(/^[ \t]*export[ \t]+/, "", line)
|
||||
name = line
|
||||
sub(/=.*/, "", name)
|
||||
name = trim(name)
|
||||
if (name == key) {
|
||||
sub(/^[^=]*=/, "", line)
|
||||
print trim(line)
|
||||
found = 1
|
||||
exit
|
||||
}
|
||||
}
|
||||
END { if (!found) exit 1 }
|
||||
' "$file") || return 1
|
||||
strip_wrapping_quotes "$raw"
|
||||
}
|
||||
|
||||
dev_config_value() {
|
||||
env_file_value "$dev_config_file" "$1"
|
||||
}
|
||||
|
||||
runtime_env_value() {
|
||||
env_file_value "$runtime_env_file" "$1"
|
||||
}
|
||||
|
||||
generated_env_value() {
|
||||
env_file_value "$generated_env_file" "$1"
|
||||
}
|
||||
|
||||
write_initial_dev_config() {
|
||||
[ ! -e "$dev_config_file" ] || return 0
|
||||
|
||||
temp_config=$dev_config_file.$$
|
||||
previous_umask=$(umask)
|
||||
umask 077
|
||||
cat >"$temp_config" <<'EOF'
|
||||
# PI WEB Docker dev settings. Safe to edit.
|
||||
#
|
||||
# docker/pi-web-docker --dev creates this file once and does not
|
||||
# overwrite it. Put persistent dev Docker settings here.
|
||||
#
|
||||
# Precedence for values used by docker/pi-web-docker --dev:
|
||||
# 1. this file
|
||||
# 2. previous generated values, when present
|
||||
# 3. current shell environment, on first generation only
|
||||
# 4. runtime installer env, usually ~/.local/share/pi-web-docker/.env
|
||||
# 5. built-in defaults
|
||||
#
|
||||
# Generated effective values are written to:
|
||||
# .pi-web/docker-compose-dev.generated.env
|
||||
#
|
||||
# Bind addresses:
|
||||
# - 127.0.0.1 exposes only to this machine.
|
||||
# - 0.0.0.0 exposes on all host interfaces. Use only on trusted networks.
|
||||
#
|
||||
# Uncomment or add values to persist them. PI_WEB_DEV_API_BIND_ADDR
|
||||
# controls the web/API server; PI_WEB_DEV_BIND_ADDR controls the Vite UI.
|
||||
# PI_WEB_DEV_API_BIND_ADDR=127.0.0.1
|
||||
# PI_WEB_DEV_BIND_ADDR=127.0.0.1
|
||||
# PI_WEB_DEV_API_PORT=8504
|
||||
# PI_WEB_DEV_PORT=8505
|
||||
#
|
||||
# Shared Docker/runtime-style defaults may also be set here:
|
||||
# PI_WEB_DOCKER_DATA_DIR=/absolute/path/to/pi-web-docker/data
|
||||
# PI_WEB_DOCKER_EXTRA_HOST_PATHS="/absolute/path/one /absolute/path/two"
|
||||
#
|
||||
# PI_WEB_UID and PI_WEB_GID default to the current host user so
|
||||
# bind-mounted checkout files are not written as root or another user.
|
||||
# Set them here only if you intentionally want a different container user.
|
||||
EOF
|
||||
umask "$previous_umask"
|
||||
|
||||
if [ -f "$legacy_dev_env_file" ]; then
|
||||
{
|
||||
printf '\n%s\n' "# Values copied from the previous generated dev env file."
|
||||
printf '%s\n' "# Keep, edit, or delete these lines as needed."
|
||||
for key in PI_WEB_DEV_API_BIND_ADDR PI_WEB_DEV_BIND_ADDR PI_WEB_DEV_API_PORT PI_WEB_DEV_PORT; do
|
||||
if value=$(env_file_value "$legacy_dev_env_file" "$key"); then
|
||||
printf '%s=%s\n' "$key" "$value"
|
||||
fi
|
||||
done
|
||||
} >>"$temp_config"
|
||||
fi
|
||||
|
||||
mv "$temp_config" "$dev_config_file"
|
||||
log "Created user-editable dev config: $dev_config_file"
|
||||
}
|
||||
|
||||
value_from_config_or_generated_or_env_or_runtime_or_default() {
|
||||
key=$1
|
||||
default_value=$2
|
||||
if existing=$(dev_config_value "$key"); then
|
||||
printf '%s\n' "$existing"
|
||||
elif existing=$(generated_env_value "$key"); then
|
||||
printf '%s\n' "$existing"
|
||||
else
|
||||
eval "is_set=\${$key+x}"
|
||||
if [ "${is_set:-}" = x ]; then
|
||||
eval "printf '%s\n' \"\${$key}\""
|
||||
elif existing=$(runtime_env_value "$key"); then
|
||||
printf '%s\n' "$existing"
|
||||
else
|
||||
printf '%s\n' "$default_value"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
value_from_config_or_generated_or_env_or_default() {
|
||||
key=$1
|
||||
default_value=$2
|
||||
if existing=$(dev_config_value "$key"); then
|
||||
printf '%s\n' "$existing"
|
||||
elif existing=$(generated_env_value "$key"); then
|
||||
printf '%s\n' "$existing"
|
||||
else
|
||||
eval "is_set=\${$key+x}"
|
||||
if [ "${is_set:-}" = x ]; then
|
||||
eval "printf '%s\n' \"\${$key}\""
|
||||
else
|
||||
printf '%s\n' "$default_value"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
is_truthy() {
|
||||
case "${1:-}" in
|
||||
""|0|false|FALSE|False) return 1 ;;
|
||||
*) return 0 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
is_unsigned_int() {
|
||||
case "${1:-}" in
|
||||
""|*[!0-9]*) return 1 ;;
|
||||
*) return 0 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
require_unsigned_int() {
|
||||
name=$1
|
||||
value=$2
|
||||
is_unsigned_int "$value" || die "$name must be a numeric Unix id, got: $value"
|
||||
}
|
||||
|
||||
enforce_dev_root_safety() {
|
||||
uid=$(id -u 2>/dev/null || printf '0')
|
||||
[ "$uid" != 0 ] || is_truthy "${PI_WEB_DOCKER_ALLOW_ROOT:-0}" || die "refusing to run Docker development mode as root; retry with --allow-root if this is intentional"
|
||||
}
|
||||
|
||||
enforce_non_root_dev_uid() {
|
||||
[ "${1:-0}" -ne 0 ] || is_truthy "${PI_WEB_DOCKER_ALLOW_ROOT:-0}" || die "refusing to generate Docker development env with PI_WEB_UID=0; retry with --allow-root if this is intentional"
|
||||
}
|
||||
|
||||
enforce_dev_root_safety
|
||||
|
||||
if ! pi_web_docker_host_detect_profile; then
|
||||
pi_web_docker_host_print_detection_failure
|
||||
die "refusing to run Docker Compose for an unsupported or unknown host setup"
|
||||
fi
|
||||
|
||||
runtime_env_file=${PI_WEB_DOCKER_RUNTIME_ENV_FILE:-}
|
||||
if [ -z "$runtime_env_file" ] && [ -n "${HOME:-}" ]; then
|
||||
runtime_env_file=$HOME/.local/share/pi-web-docker/.env
|
||||
fi
|
||||
|
||||
mkdir -p "$repo_root/.pi-web" || die "could not create .pi-web directory"
|
||||
write_initial_dev_config
|
||||
|
||||
host_uid=$(id -u 2>/dev/null || printf '0')
|
||||
host_gid=$(id -g 2>/dev/null || printf '0')
|
||||
pi_web_uid=$(value_from_config_or_generated_or_env_or_default PI_WEB_UID "$host_uid")
|
||||
pi_web_gid=$(value_from_config_or_generated_or_env_or_default PI_WEB_GID "$host_gid")
|
||||
docker_gid=$(value_from_config_or_generated_or_env_or_runtime_or_default DOCKER_GID "$(pi_web_docker_host_detect_docker_gid)")
|
||||
default_data_dir=${HOME:-$repo_root/.pi-web}/.local/share/pi-web-docker/data
|
||||
pi_web_data_dir=$(value_from_config_or_generated_or_env_or_runtime_or_default PI_WEB_DOCKER_DATA_DIR "$default_data_dir")
|
||||
pi_web_extra_host_paths=$(value_from_config_or_generated_or_env_or_runtime_or_default PI_WEB_DOCKER_EXTRA_HOST_PATHS "")
|
||||
pi_web_opensuse_image=$(value_from_config_or_generated_or_env_or_runtime_or_default PI_WEB_OPENSUSE_IMAGE opensuse/tumbleweed)
|
||||
pi_web_nodejs_major=$(value_from_config_or_generated_or_env_or_runtime_or_default PI_WEB_NODEJS_MAJOR 22)
|
||||
pi_web_nodejs_repo=$(value_from_config_or_generated_or_env_or_runtime_or_default PI_WEB_NODEJS_REPO auto)
|
||||
pi_web_extra_zypper_packages=$(value_from_config_or_generated_or_env_or_runtime_or_default PI_WEB_EXTRA_ZYPPER_PACKAGES "")
|
||||
pi_web_dev_image=$(value_from_config_or_generated_or_env_or_runtime_or_default PI_WEB_DEV_IMAGE pi-web:dev)
|
||||
compose_project_name=$(value_from_config_or_generated_or_env_or_default COMPOSE_PROJECT_NAME pi-web-dev)
|
||||
hostexec_image=$(value_from_config_or_generated_or_env_or_runtime_or_default HOSTEXEC_IMAGE alpine:3.22)
|
||||
pi_web_max_upload_bytes=$(value_from_config_or_generated_or_env_or_runtime_or_default PI_WEB_MAX_UPLOAD_BYTES 67108864)
|
||||
default_dev_bind_addr=$(value_from_config_or_generated_or_env_or_runtime_or_default PI_WEB_BIND_ADDR 127.0.0.1)
|
||||
pi_web_dev_api_bind_addr=$(value_from_config_or_generated_or_env_or_runtime_or_default PI_WEB_DEV_API_BIND_ADDR "$default_dev_bind_addr")
|
||||
pi_web_dev_bind_addr=$(value_from_config_or_generated_or_env_or_runtime_or_default PI_WEB_DEV_BIND_ADDR "$default_dev_bind_addr")
|
||||
pi_web_dev_api_port=$(value_from_config_or_generated_or_env_or_runtime_or_default PI_WEB_DEV_API_PORT 8504)
|
||||
pi_web_dev_port=$(value_from_config_or_generated_or_env_or_runtime_or_default PI_WEB_DEV_PORT 8505)
|
||||
|
||||
require_unsigned_int PI_WEB_UID "$pi_web_uid"
|
||||
require_unsigned_int PI_WEB_GID "$pi_web_gid"
|
||||
require_unsigned_int DOCKER_GID "$docker_gid"
|
||||
enforce_non_root_dev_uid "$pi_web_uid"
|
||||
case "$pi_web_data_dir" in
|
||||
/*) ;;
|
||||
*) die "PI_WEB_DOCKER_DATA_DIR must be an absolute path, got: $pi_web_data_dir" ;;
|
||||
esac
|
||||
[ -n "$compose_project_name" ] || die "COMPOSE_PROJECT_NAME must not be empty"
|
||||
|
||||
mkdir -p "$pi_web_data_dir" || die "could not create data directory: $pi_web_data_dir"
|
||||
|
||||
env_file=$generated_env_file
|
||||
override_file=$repo_root/.pi-web/docker-compose-dev.host.generated.yml
|
||||
|
||||
if ! pi_web_docker_host_write_compose_override "$override_file" "$PI_WEB_DETECTED_DOCKER_HOST_PROFILE" "$pi_web_extra_host_paths" "$repo_root"; then
|
||||
die "could not write host-specific Compose override"
|
||||
fi
|
||||
|
||||
umask 077
|
||||
temp_env=$env_file.$$
|
||||
cat >"$temp_env" <<EOF
|
||||
# Generated by docker/pi-web-docker --dev. Do not edit by hand.
|
||||
PI_WEB_UID=$pi_web_uid
|
||||
PI_WEB_GID=$pi_web_gid
|
||||
DOCKER_GID=$docker_gid
|
||||
PI_WEB_DOCKER_DATA_DIR=$pi_web_data_dir
|
||||
PI_WEB_DOCKER_DEV_REPO_ROOT=$repo_root
|
||||
PI_WEB_DOCKER_HOST_PROFILE=$PI_WEB_DETECTED_DOCKER_HOST_PROFILE
|
||||
HOSTEXEC_MODE=$PI_WEB_DETECTED_HOSTEXEC_MODE
|
||||
PI_WEB_DOCKER_EXTRA_HOST_PATHS=$pi_web_extra_host_paths
|
||||
PI_WEB_OPENSUSE_IMAGE=$pi_web_opensuse_image
|
||||
PI_WEB_NODEJS_MAJOR=$pi_web_nodejs_major
|
||||
PI_WEB_NODEJS_REPO=$pi_web_nodejs_repo
|
||||
PI_WEB_EXTRA_ZYPPER_PACKAGES=$pi_web_extra_zypper_packages
|
||||
PI_WEB_DEV_IMAGE=$pi_web_dev_image
|
||||
COMPOSE_PROJECT_NAME=$compose_project_name
|
||||
HOSTEXEC_IMAGE=$hostexec_image
|
||||
PI_WEB_MAX_UPLOAD_BYTES=$pi_web_max_upload_bytes
|
||||
PI_WEB_DEV_API_BIND_ADDR=$pi_web_dev_api_bind_addr
|
||||
PI_WEB_DEV_BIND_ADDR=$pi_web_dev_bind_addr
|
||||
PI_WEB_DEV_API_PORT=$pi_web_dev_api_port
|
||||
PI_WEB_DEV_PORT=$pi_web_dev_port
|
||||
EOF
|
||||
mv "$temp_env" "$env_file"
|
||||
|
||||
log "Selected PI WEB Docker host profile: $PI_WEB_DETECTED_DOCKER_HOST_PROFILE"
|
||||
if [ -f "$runtime_env_file" ]; then
|
||||
log "Reused runtime Docker environment defaults from: $runtime_env_file"
|
||||
fi
|
||||
case "$PI_WEB_DETECTED_DOCKER_HOST_PROFILE" in
|
||||
linux-native-docker)
|
||||
log "Enabled Linux host mounts and hostexec namespace bridge."
|
||||
;;
|
||||
mac-docker-desktop)
|
||||
log "Enabled Docker Desktop for Mac project mounts. hostexec is disabled because containers cannot enter native macOS namespaces."
|
||||
;;
|
||||
esac
|
||||
log "User-editable dev config: $dev_config_file"
|
||||
log "Generated dev env: $env_file"
|
||||
log "Generated dev Compose override: $override_file"
|
||||
|
||||
if [ "$#" -eq 0 ]; then
|
||||
set -- up --build
|
||||
fi
|
||||
|
||||
pi_web_docker_compose \
|
||||
--project-name "$compose_project_name" \
|
||||
--env-file "$env_file" \
|
||||
-f "$repo_root/docker/compose.dev.yml" \
|
||||
-f "$override_file" \
|
||||
"$@"
|
||||
@@ -0,0 +1,365 @@
|
||||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034
|
||||
|
||||
pi_web_docker_host_yaml_quote() {
|
||||
value=$1
|
||||
escaped=$(printf '%s' "$value" | sed "s/'/''/g")
|
||||
printf "'%s'" "$escaped"
|
||||
}
|
||||
|
||||
pi_web_docker_host_socket_path_from_endpoint() {
|
||||
endpoint=$1
|
||||
case "$endpoint" in
|
||||
unix://*) printf '%s\n' "${endpoint#unix://}" ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
pi_web_docker_host_mac_desktop_socket_path() {
|
||||
[ -n "${HOME:-}" ] || return 1
|
||||
printf '%s/.docker/run/docker.sock\n' "$HOME"
|
||||
}
|
||||
|
||||
pi_web_docker_host_endpoint_is_linux_expected() {
|
||||
endpoint=$1
|
||||
[ "$endpoint" = unix:///var/run/docker.sock ]
|
||||
}
|
||||
|
||||
pi_web_docker_host_endpoint_is_mac_expected() {
|
||||
endpoint=$1
|
||||
if ! socket_path=$(pi_web_docker_host_socket_path_from_endpoint "$endpoint" 2>/dev/null); then
|
||||
return 1
|
||||
fi
|
||||
|
||||
case "$socket_path" in
|
||||
/var/run/docker.sock)
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
if mac_socket_path=$(pi_web_docker_host_mac_desktop_socket_path 2>/dev/null); then
|
||||
[ "$socket_path" = "$mac_socket_path" ] && return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
pi_web_docker_host_socket_source_for_endpoint() {
|
||||
endpoint=$1
|
||||
pi_web_docker_host_socket_path_from_endpoint "$endpoint"
|
||||
}
|
||||
|
||||
pi_web_docker_host_detect_docker_gid() {
|
||||
case "${PI_WEB_DETECTED_DOCKER_HOST_PROFILE:-}" in
|
||||
mac-docker-desktop)
|
||||
printf '0\n'
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
socket_path=/var/run/docker.sock
|
||||
if [ -n "${PI_WEB_DETECTED_DOCKER_ENDPOINT:-}" ]; then
|
||||
if detected_socket_path=$(pi_web_docker_host_socket_path_from_endpoint "$PI_WEB_DETECTED_DOCKER_ENDPOINT" 2>/dev/null); then
|
||||
socket_path=$detected_socket_path
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -S "$socket_path" ]; then
|
||||
if gid=$(stat -c '%g' "$socket_path" 2>/dev/null); then
|
||||
printf '%s\n' "$gid"
|
||||
return 0
|
||||
fi
|
||||
if gid=$(stat -f '%g' "$socket_path" 2>/dev/null); then
|
||||
printf '%s\n' "$gid"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -S /var/run/docker.sock ]; then
|
||||
if gid=$(stat -c '%g' /var/run/docker.sock 2>/dev/null); then
|
||||
printf '%s\n' "$gid"
|
||||
return 0
|
||||
fi
|
||||
if gid=$(stat -f '%g' /var/run/docker.sock 2>/dev/null); then
|
||||
printf '%s\n' "$gid"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if command -v getent >/dev/null 2>&1; then
|
||||
if gid=$(getent group docker | awk -F: 'NR == 1 { print $3 }'); then
|
||||
if [ -n "$gid" ]; then
|
||||
printf '%s\n' "$gid"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
printf '0\n'
|
||||
}
|
||||
|
||||
pi_web_docker_host_detect_profile() {
|
||||
PI_WEB_DETECTED_HOST_OS=$(uname -s 2>/dev/null || printf 'unknown')
|
||||
PI_WEB_DETECTED_DOCKER_CONTEXT=
|
||||
PI_WEB_DETECTED_DOCKER_ENDPOINT=
|
||||
PI_WEB_DETECTED_DOCKER_HOST_ENV=${DOCKER_HOST:-}
|
||||
PI_WEB_DETECTED_DOCKER_EFFECTIVE_ENDPOINT=
|
||||
PI_WEB_DETECTED_DOCKER_SOCKET_SOURCE=
|
||||
PI_WEB_DETECTED_DOCKER_OS=
|
||||
PI_WEB_DETECTED_DOCKER_HOST_PROFILE=
|
||||
PI_WEB_DETECTED_HOSTEXEC_MODE=disabled
|
||||
PI_WEB_DOCKER_HOST_PROFILE_ERROR=
|
||||
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
PI_WEB_DOCKER_HOST_PROFILE_ERROR="docker CLI is required"
|
||||
return 1
|
||||
fi
|
||||
|
||||
PI_WEB_DETECTED_DOCKER_CONTEXT=$(docker context show 2>/dev/null || printf 'unknown')
|
||||
if [ -n "$PI_WEB_DETECTED_DOCKER_CONTEXT" ] && [ "$PI_WEB_DETECTED_DOCKER_CONTEXT" != unknown ]; then
|
||||
PI_WEB_DETECTED_DOCKER_ENDPOINT=$(docker context inspect "$PI_WEB_DETECTED_DOCKER_CONTEXT" --format '{{if .Endpoints.docker}}{{.Endpoints.docker.Host}}{{end}}' 2>/dev/null || printf '')
|
||||
fi
|
||||
|
||||
case "$PI_WEB_DETECTED_HOST_OS" in
|
||||
Linux)
|
||||
if [ -n "$PI_WEB_DETECTED_DOCKER_HOST_ENV" ] && ! pi_web_docker_host_endpoint_is_linux_expected "$PI_WEB_DETECTED_DOCKER_HOST_ENV"; then
|
||||
PI_WEB_DOCKER_HOST_PROFILE_ERROR="native Linux installs require DOCKER_HOST to be unset or exactly unix:///var/run/docker.sock, not $PI_WEB_DETECTED_DOCKER_HOST_ENV"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -n "$PI_WEB_DETECTED_DOCKER_ENDPOINT" ] && ! pi_web_docker_host_endpoint_is_linux_expected "$PI_WEB_DETECTED_DOCKER_ENDPOINT"; then
|
||||
PI_WEB_DOCKER_HOST_PROFILE_ERROR="native Linux installs require the local /var/run/docker.sock Docker context, not $PI_WEB_DETECTED_DOCKER_ENDPOINT"
|
||||
return 1
|
||||
fi
|
||||
|
||||
PI_WEB_DETECTED_DOCKER_EFFECTIVE_ENDPOINT=${PI_WEB_DETECTED_DOCKER_HOST_ENV:-$PI_WEB_DETECTED_DOCKER_ENDPOINT}
|
||||
PI_WEB_DETECTED_DOCKER_SOCKET_SOURCE=/var/run/docker.sock
|
||||
if [ ! -S "$PI_WEB_DETECTED_DOCKER_SOCKET_SOURCE" ]; then
|
||||
PI_WEB_DOCKER_HOST_PROFILE_ERROR="native Linux installs require a local Docker socket at /var/run/docker.sock"
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
Darwin)
|
||||
if [ -n "$PI_WEB_DETECTED_DOCKER_ENDPOINT" ] && ! pi_web_docker_host_endpoint_is_mac_expected "$PI_WEB_DETECTED_DOCKER_ENDPOINT"; then
|
||||
PI_WEB_DOCKER_HOST_PROFILE_ERROR="macOS installs require a Docker Desktop local Unix socket context, not $PI_WEB_DETECTED_DOCKER_ENDPOINT"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -n "$PI_WEB_DETECTED_DOCKER_HOST_ENV" ]; then
|
||||
if ! pi_web_docker_host_endpoint_is_mac_expected "$PI_WEB_DETECTED_DOCKER_HOST_ENV"; then
|
||||
PI_WEB_DOCKER_HOST_PROFILE_ERROR="macOS installs require DOCKER_HOST to be unset or a Docker Desktop local Unix socket, not $PI_WEB_DETECTED_DOCKER_HOST_ENV"
|
||||
return 1
|
||||
fi
|
||||
PI_WEB_DETECTED_DOCKER_EFFECTIVE_ENDPOINT=$PI_WEB_DETECTED_DOCKER_HOST_ENV
|
||||
else
|
||||
PI_WEB_DETECTED_DOCKER_EFFECTIVE_ENDPOINT=$PI_WEB_DETECTED_DOCKER_ENDPOINT
|
||||
fi
|
||||
|
||||
if [ -n "$PI_WEB_DETECTED_DOCKER_EFFECTIVE_ENDPOINT" ]; then
|
||||
if ! pi_web_docker_host_endpoint_is_mac_expected "$PI_WEB_DETECTED_DOCKER_EFFECTIVE_ENDPOINT"; then
|
||||
PI_WEB_DOCKER_HOST_PROFILE_ERROR="macOS installs require a Docker Desktop local Unix socket, not ${PI_WEB_DETECTED_DOCKER_EFFECTIVE_ENDPOINT:-unknown}"
|
||||
return 1
|
||||
fi
|
||||
PI_WEB_DETECTED_DOCKER_SOCKET_SOURCE=$(pi_web_docker_host_socket_source_for_endpoint "$PI_WEB_DETECTED_DOCKER_EFFECTIVE_ENDPOINT") || return 1
|
||||
elif mac_socket_path=$(pi_web_docker_host_mac_desktop_socket_path 2>/dev/null) && [ -S "$mac_socket_path" ]; then
|
||||
PI_WEB_DETECTED_DOCKER_SOCKET_SOURCE=$mac_socket_path
|
||||
else
|
||||
PI_WEB_DETECTED_DOCKER_SOCKET_SOURCE=/var/run/docker.sock
|
||||
fi
|
||||
|
||||
if [ ! -S "$PI_WEB_DETECTED_DOCKER_SOCKET_SOURCE" ]; then
|
||||
PI_WEB_DOCKER_HOST_PROFILE_ERROR="Docker Desktop socket is not accessible at $PI_WEB_DETECTED_DOCKER_SOCKET_SOURCE"
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
PI_WEB_DOCKER_HOST_PROFILE_ERROR="unsupported host OS: $PI_WEB_DETECTED_HOST_OS"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if ! docker info >/dev/null 2>&1; then
|
||||
PI_WEB_DOCKER_HOST_PROFILE_ERROR="docker daemon is not reachable by this user"
|
||||
return 1
|
||||
fi
|
||||
PI_WEB_DETECTED_DOCKER_OS=$(docker info --format '{{.OperatingSystem}}' 2>/dev/null || printf '')
|
||||
|
||||
case "$PI_WEB_DETECTED_HOST_OS" in
|
||||
Linux)
|
||||
case "$PI_WEB_DETECTED_DOCKER_CONTEXT:$PI_WEB_DETECTED_DOCKER_OS" in
|
||||
*desktop-linux*|*"Docker Desktop"*)
|
||||
PI_WEB_DOCKER_HOST_PROFILE_ERROR="Docker Desktop on Linux is not supported by this installer because it runs containers inside a VM instead of the native Linux host"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
PI_WEB_DETECTED_DOCKER_HOST_PROFILE=linux-native-docker
|
||||
PI_WEB_DETECTED_HOSTEXEC_MODE=nsenter
|
||||
;;
|
||||
Darwin)
|
||||
case "$PI_WEB_DETECTED_DOCKER_CONTEXT:$PI_WEB_DETECTED_DOCKER_OS:$PI_WEB_DETECTED_DOCKER_EFFECTIVE_ENDPOINT" in
|
||||
*desktop-linux*|*"Docker Desktop"*|*"/.docker/run/docker.sock"*)
|
||||
PI_WEB_DETECTED_DOCKER_HOST_PROFILE=mac-docker-desktop
|
||||
PI_WEB_DETECTED_HOSTEXEC_MODE=disabled
|
||||
;;
|
||||
*)
|
||||
PI_WEB_DOCKER_HOST_PROFILE_ERROR="macOS installs currently require Docker Desktop; detected context '$PI_WEB_DETECTED_DOCKER_CONTEXT' endpoint '${PI_WEB_DETECTED_DOCKER_EFFECTIVE_ENDPOINT:-unknown}'"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
pi_web_docker_host_write_volume() {
|
||||
source_path=$1
|
||||
target_path=$2
|
||||
read_only=${3:-false}
|
||||
|
||||
{
|
||||
printf ' - type: bind\n'
|
||||
printf ' source: %s\n' "$(pi_web_docker_host_yaml_quote "$source_path")"
|
||||
printf ' target: %s\n' "$(pi_web_docker_host_yaml_quote "$target_path")"
|
||||
if [ "$read_only" = true ]; then
|
||||
printf ' read_only: true\n'
|
||||
fi
|
||||
} >>"$PI_WEB_DOCKER_HOST_OVERRIDE_TEMP"
|
||||
}
|
||||
|
||||
pi_web_docker_host_write_existing_volume() {
|
||||
source_path=$1
|
||||
target_path=$2
|
||||
read_only=${3:-false}
|
||||
|
||||
if [ -e "$source_path" ]; then
|
||||
pi_web_docker_host_write_volume "$source_path" "$target_path" "$read_only"
|
||||
fi
|
||||
}
|
||||
|
||||
pi_web_docker_host_write_extra_volumes() {
|
||||
extra_paths=$1
|
||||
|
||||
for extra_path in $extra_paths; do
|
||||
case "$extra_path" in
|
||||
/*) ;;
|
||||
*)
|
||||
printf '%s\n' "PI_WEB_DOCKER_EXTRA_HOST_PATHS entries must be absolute paths: $extra_path" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ ! -e "$extra_path" ]; then
|
||||
printf '%s\n' "PI_WEB_DOCKER_EXTRA_HOST_PATHS entry does not exist: $extra_path" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
pi_web_docker_host_write_volume "$extra_path" "$extra_path" false
|
||||
done
|
||||
}
|
||||
|
||||
pi_web_docker_host_write_compose_override() {
|
||||
target_file=$1
|
||||
host_profile=$2
|
||||
extra_paths=${3:-}
|
||||
control_path=${4:-}
|
||||
target_dir=$(dirname "$target_file")
|
||||
mkdir -p "$target_dir" || return 1
|
||||
PI_WEB_DOCKER_HOST_OVERRIDE_TEMP=$target_file.$$
|
||||
|
||||
case "$host_profile" in
|
||||
linux-native-docker) hostexec_mode=nsenter ;;
|
||||
mac-docker-desktop) hostexec_mode=disabled ;;
|
||||
*)
|
||||
printf '%s\n' "unsupported PI WEB Docker host profile: $host_profile" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
cat >"$PI_WEB_DOCKER_HOST_OVERRIDE_TEMP" <<EOF
|
||||
# Generated by PI WEB Docker host profile detection. Do not edit by hand.
|
||||
# Re-run the installer or docker/pi-web-docker --dev to refresh this file.
|
||||
|
||||
x-pi-web-host-volumes: &pi-web-host-volumes
|
||||
EOF
|
||||
|
||||
socket_source=${PI_WEB_DETECTED_DOCKER_SOCKET_SOURCE:-/var/run/docker.sock}
|
||||
pi_web_docker_host_write_volume "$socket_source" /var/run/docker.sock false
|
||||
|
||||
case "$host_profile" in
|
||||
linux-native-docker)
|
||||
pi_web_docker_host_write_existing_volume /home /home false
|
||||
pi_web_docker_host_write_existing_volume /srv /srv false
|
||||
pi_web_docker_host_write_existing_volume /opt /opt false
|
||||
pi_web_docker_host_write_volume / /host true
|
||||
;;
|
||||
mac-docker-desktop)
|
||||
pi_web_docker_host_write_existing_volume /Users /Users false
|
||||
pi_web_docker_host_write_existing_volume /Volumes /Volumes false
|
||||
pi_web_docker_host_write_existing_volume /private /private false
|
||||
;;
|
||||
esac
|
||||
|
||||
if ! pi_web_docker_host_write_extra_volumes "$extra_paths"; then
|
||||
rm -f "$PI_WEB_DOCKER_HOST_OVERRIDE_TEMP"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -n "$control_path" ]; then
|
||||
if [ ! -e "$control_path" ]; then
|
||||
printf '%s\n' "PI WEB Docker control path does not exist: $control_path" >&2
|
||||
rm -f "$PI_WEB_DOCKER_HOST_OVERRIDE_TEMP"
|
||||
return 1
|
||||
fi
|
||||
pi_web_docker_host_write_volume "$control_path" "$control_path" false
|
||||
fi
|
||||
|
||||
cat >>"$PI_WEB_DOCKER_HOST_OVERRIDE_TEMP" <<EOF
|
||||
|
||||
services:
|
||||
sessiond:
|
||||
environment:
|
||||
HOSTEXEC_MODE: $hostexec_mode
|
||||
volumes: *pi-web-host-volumes
|
||||
|
||||
web:
|
||||
environment:
|
||||
HOSTEXEC_MODE: $hostexec_mode
|
||||
volumes: *pi-web-host-volumes
|
||||
EOF
|
||||
|
||||
mv "$PI_WEB_DOCKER_HOST_OVERRIDE_TEMP" "$target_file"
|
||||
}
|
||||
|
||||
pi_web_docker_host_print_detection_failure() {
|
||||
printf '%s\n' "PI WEB Docker setup could not determine a supported host profile." >&2
|
||||
printf '%s\n' "" >&2
|
||||
printf '%s\n' "Detected:" >&2
|
||||
printf ' host OS: %s\n' "${PI_WEB_DETECTED_HOST_OS:-unknown}" >&2
|
||||
printf ' docker context: %s\n' "${PI_WEB_DETECTED_DOCKER_CONTEXT:-unknown}" >&2
|
||||
printf ' docker endpoint: %s\n' "${PI_WEB_DETECTED_DOCKER_ENDPOINT:-unknown}" >&2
|
||||
printf ' DOCKER_HOST: %s\n' "${PI_WEB_DETECTED_DOCKER_HOST_ENV:-unset}" >&2
|
||||
printf ' effective endpoint: %s\n' "${PI_WEB_DETECTED_DOCKER_EFFECTIVE_ENDPOINT:-unknown}" >&2
|
||||
printf ' docker socket source: %s\n' "${PI_WEB_DETECTED_DOCKER_SOCKET_SOURCE:-unknown}" >&2
|
||||
printf ' docker OS: %s\n' "${PI_WEB_DETECTED_DOCKER_OS:-unknown}" >&2
|
||||
printf '%s\n' "" >&2
|
||||
printf '%s\n' "Supported profiles:" >&2
|
||||
printf '%s\n' " - native Linux Docker Engine using /var/run/docker.sock" >&2
|
||||
printf '%s\n' " - Docker Desktop for Mac" >&2
|
||||
if [ -n "${PI_WEB_DOCKER_HOST_PROFILE_ERROR:-}" ]; then
|
||||
printf '%s\n' "" >&2
|
||||
printf 'Reason: %s\n' "$PI_WEB_DOCKER_HOST_PROFILE_ERROR" >&2
|
||||
fi
|
||||
}
|
||||
|
||||
pi_web_docker_compose() {
|
||||
if docker compose version >/dev/null 2>&1; then
|
||||
docker compose "$@"
|
||||
elif command -v docker-compose >/dev/null 2>&1; then
|
||||
docker-compose "$@"
|
||||
else
|
||||
printf '%s\n' "Docker Compose is required (docker compose plugin or docker-compose)" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
Executable
+161
@@ -0,0 +1,161 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
nodejs_major=${NODEJS_MAJOR:-22}
|
||||
nodejs_repo=${NODEJS_REPO:-auto}
|
||||
extra_zypper_packages=${PI_WEB_EXTRA_ZYPPER_PACKAGES:-}
|
||||
runtime_uid=${PI_WEB_UID:-1000}
|
||||
runtime_gid=${PI_WEB_GID:-1000}
|
||||
|
||||
nodejs_repo_flavor() {
|
||||
local rpm_arch
|
||||
rpm_arch=$(rpm --eval '%{_target_cpu}')
|
||||
|
||||
case "$rpm_arch" in
|
||||
aarch64|armv6hl|armv7hl)
|
||||
printf '%s\n' openSUSE_Factory_ARM
|
||||
;;
|
||||
ppc64le)
|
||||
printf '%s\n' openSUSE_Factory_PowerPC
|
||||
;;
|
||||
riscv64)
|
||||
printf '%s\n' openSUSE_Factory_RISCV
|
||||
;;
|
||||
s390x)
|
||||
printf '%s\n' openSUSE_Factory_zSystems
|
||||
;;
|
||||
*)
|
||||
printf '%s\n' openSUSE_Tumbleweed
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
add_nodejs_repo() {
|
||||
local repo_url
|
||||
|
||||
case "$nodejs_repo" in
|
||||
""|disabled|none)
|
||||
return 0
|
||||
;;
|
||||
auto)
|
||||
repo_url="https://download.opensuse.org/repositories/devel:/languages:/nodejs/$(nodejs_repo_flavor)/"
|
||||
;;
|
||||
*)
|
||||
repo_url=$nodejs_repo
|
||||
;;
|
||||
esac
|
||||
|
||||
zypper --non-interactive removerepo pi-web-nodejs >/dev/null 2>&1 || true
|
||||
zypper --non-interactive addrepo --refresh "$repo_url" pi-web-nodejs
|
||||
}
|
||||
|
||||
# The codec repository is not needed for this image and can make noninteractive
|
||||
# refreshes noisy or brittle when its signing key rolls independently.
|
||||
zypper --non-interactive modifyrepo --disable repo-openh264 >/dev/null 2>&1 || true
|
||||
|
||||
add_nodejs_repo
|
||||
zypper --gpg-auto-import-keys --non-interactive refresh
|
||||
|
||||
packages=(
|
||||
"nodejs${nodejs_major}"
|
||||
"npm${nodejs_major}"
|
||||
"corepack${nodejs_major}"
|
||||
"nodejs${nodejs_major}-devel"
|
||||
bash
|
||||
ca-certificates
|
||||
curl
|
||||
wget
|
||||
git
|
||||
git-lfs
|
||||
gh
|
||||
openssh-clients
|
||||
procps
|
||||
tini
|
||||
shadow
|
||||
gcc-c++
|
||||
make
|
||||
python3
|
||||
python3-devel
|
||||
python3-pip
|
||||
python3-virtualenv
|
||||
jq
|
||||
ripgrep
|
||||
fd
|
||||
fzf
|
||||
bat
|
||||
ShellCheck
|
||||
less
|
||||
file
|
||||
which
|
||||
tar
|
||||
gzip
|
||||
xz
|
||||
unzip
|
||||
zip
|
||||
zstd
|
||||
findutils
|
||||
grep
|
||||
sed
|
||||
gawk
|
||||
patch
|
||||
diffutils
|
||||
util-linux
|
||||
hostname
|
||||
iproute2
|
||||
bind-utils
|
||||
rsync
|
||||
)
|
||||
|
||||
extra_packages=()
|
||||
if [ -n "$extra_zypper_packages" ]; then
|
||||
# Intentionally split a whitespace-delimited package list supplied as a Docker
|
||||
# build arg, e.g. PI_WEB_EXTRA_ZYPPER_PACKAGES="go rustup kubernetes-client".
|
||||
# shellcheck disable=SC2206
|
||||
extra_packages=($extra_zypper_packages)
|
||||
fi
|
||||
|
||||
zypper --non-interactive install --no-recommends "${packages[@]}" "${extra_packages[@]}"
|
||||
|
||||
node --version
|
||||
npm --version
|
||||
npx --version
|
||||
python3 --version
|
||||
git --version
|
||||
|
||||
case "$runtime_uid" in
|
||||
""|*[!0-9]*)
|
||||
echo "PI_WEB_UID must be a numeric user ID, got: $runtime_uid" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$runtime_gid" in
|
||||
""|*[!0-9]*)
|
||||
echo "PI_WEB_GID must be a numeric group ID, got: $runtime_gid" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
runtime_user=pi-web
|
||||
runtime_group=pi-web
|
||||
runtime_home=/data/home
|
||||
mkdir -p "$runtime_home" /data/config /data/npm-cache /data/pi-web /data/pi-agent /workspace
|
||||
|
||||
if getent group "$runtime_gid" >/dev/null 2>&1; then
|
||||
runtime_group=$(getent group "$runtime_gid" | cut -d: -f1)
|
||||
elif getent group "$runtime_group" >/dev/null 2>&1; then
|
||||
groupmod --gid "$runtime_gid" "$runtime_group"
|
||||
else
|
||||
groupadd --gid "$runtime_gid" "$runtime_group"
|
||||
fi
|
||||
|
||||
if id "$runtime_user" >/dev/null 2>&1; then
|
||||
usermod --non-unique --uid "$runtime_uid" --gid "$runtime_group" --home "$runtime_home" --shell /bin/bash "$runtime_user"
|
||||
else
|
||||
useradd --non-unique --uid "$runtime_uid" --gid "$runtime_group" --no-create-home --home-dir "$runtime_home" --shell /bin/bash "$runtime_user"
|
||||
fi
|
||||
|
||||
chown -R "$runtime_uid:$runtime_gid" /data /workspace
|
||||
|
||||
zypper clean --all
|
||||
rm -rf /var/cache/zypp/*
|
||||
Reference in New Issue
Block a user