docs(kit): explain sandbox script behavior
This commit is contained in:
59
build-sbx.sh
59
build-sbx.sh
@@ -1,11 +1,17 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
# Build and optionally configure a Docker sandbox interactively.
|
# Build and optionally configure a Docker sandbox interactively.
|
||||||
|
#
|
||||||
|
# The workflow deliberately asks before making choices that affect the new
|
||||||
|
# sandbox. This makes it harder to create a sandbox with the wrong name,
|
||||||
|
# workspace directories, copied data, or credentials.
|
||||||
|
|
||||||
|
# Stop on command failures, unset variables, and failures hidden in pipelines.
|
||||||
set -o errexit
|
set -o errexit
|
||||||
set -o nounset
|
set -o nounset
|
||||||
set -o pipefail
|
set -o pipefail
|
||||||
|
|
||||||
|
# These values are readonly because they are configuration, not user input.
|
||||||
readonly KIT_PATH="$HOME/.sbx/kits/hypoport.ai"
|
readonly KIT_PATH="$HOME/.sbx/kits/hypoport.ai"
|
||||||
readonly TEMPLATE='127.0.0.1:62735/finmas.de/mux033/opencode-ops:latest'
|
readonly TEMPLATE='127.0.0.1:62735/finmas.de/mux033/opencode-ops:latest'
|
||||||
|
|
||||||
@@ -13,6 +19,8 @@ require_command() {
|
|||||||
local command_name="$1"
|
local command_name="$1"
|
||||||
local message="$2"
|
local message="$2"
|
||||||
|
|
||||||
|
# `command -v` checks the current PATH without printing anything. The
|
||||||
|
# explicit error is easier to understand than a later command-not-found.
|
||||||
if ! command -v "$command_name" >/dev/null 2>&1; then
|
if ! command -v "$command_name" >/dev/null 2>&1; then
|
||||||
printf '%s\n' "$message" >&2
|
printf '%s\n' "$message" >&2
|
||||||
exit 1
|
exit 1
|
||||||
@@ -23,6 +31,8 @@ ask_yes_no() {
|
|||||||
local prompt="$1"
|
local prompt="$1"
|
||||||
local answer
|
local answer
|
||||||
|
|
||||||
|
# Returning success for yes and failure for no lets callers use this
|
||||||
|
# function directly as the condition of an `if` statement.
|
||||||
while true; do
|
while true; do
|
||||||
read -r -p "$prompt [y/n] " answer || exit 0
|
read -r -p "$prompt [y/n] " answer || exit 0
|
||||||
case "$answer" in
|
case "$answer" in
|
||||||
@@ -34,14 +44,21 @@ ask_yes_no() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
prompt_component() {
|
prompt_component() {
|
||||||
|
# Bash functions normally cannot assign to a caller's local variable. The
|
||||||
|
# first argument is therefore a variable name, which `printf -v` assigns
|
||||||
|
# to without using unsafe `eval`.
|
||||||
local result_var="$1"
|
local result_var="$1"
|
||||||
local label="$2"
|
local label="$2"
|
||||||
shift 2
|
shift 2
|
||||||
|
# Arrays keep each menu option as one value, even if it contains spaces.
|
||||||
|
# `custom` is a synthetic final option for values not listed in advance.
|
||||||
local options=("$@" custom)
|
local options=("$@" custom)
|
||||||
local choice custom_value
|
local choice custom_value
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
printf '%s:\n' "$label"
|
printf '%s:\n' "$label"
|
||||||
|
# `select` prints a numbered menu and puts the selected text in
|
||||||
|
# `choice`. Invalid numbers produce an empty choice.
|
||||||
select choice in "${options[@]}"; do
|
select choice in "${options[@]}"; do
|
||||||
case "$choice" in
|
case "$choice" in
|
||||||
custom)
|
custom)
|
||||||
@@ -63,6 +80,8 @@ prompt_component() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
|
# Check dependencies before asking questions. A missing tool should not
|
||||||
|
# leave the user halfway through an interactive setup.
|
||||||
require_command sbx 'We cannot build a Docker sbx image because sbx does not seem to be installed.'
|
require_command sbx 'We cannot build a Docker sbx image because sbx does not seem to be installed.'
|
||||||
require_command jq 'We cannot list existing Docker sbx sandboxes because jq does not seem to be installed.'
|
require_command jq 'We cannot list existing Docker sbx sandboxes because jq does not seem to be installed.'
|
||||||
|
|
||||||
@@ -74,11 +93,16 @@ main() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
local sandbox_json
|
local sandbox_json
|
||||||
|
# Capture the inventory once. The same snapshot is used for source
|
||||||
|
# selection and cleanup so the cleanup menu cannot unexpectedly include
|
||||||
|
# the sandbox created by this run.
|
||||||
if ! sandbox_json=$(sbx ls --json); then
|
if ! sandbox_json=$(sbx ls --json); then
|
||||||
printf 'Unable to retrieve the existing Docker sbx sandboxes.\n' >&2
|
printf 'Unable to retrieve the existing Docker sbx sandboxes.\n' >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# A successful command can still return malformed data. `-s` treats the
|
||||||
|
# input as one stream and `-e` makes jq fail when the validation is false.
|
||||||
if ! jq -s -e 'length == 1 and (.[0].sandboxes | type == "array")' \
|
if ! jq -s -e 'length == 1 and (.[0].sandboxes | type == "array")' \
|
||||||
<<< "$sandbox_json" >/dev/null; then
|
<<< "$sandbox_json" >/dev/null; then
|
||||||
printf 'The output from sbx ls --json was not the expected JSON format.\n' >&2
|
printf 'The output from sbx ls --json was not the expected JSON format.\n' >&2
|
||||||
@@ -86,6 +110,8 @@ main() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
local -a sandbox_names
|
local -a sandbox_names
|
||||||
|
# `mapfile -t` turns one newline-delimited name per line into a Bash array.
|
||||||
|
# `-r` prevents jq from adding JSON quotes around the names.
|
||||||
mapfile -t sandbox_names < <(jq -r '.sandboxes[].name' <<< "$sandbox_json")
|
mapfile -t sandbox_names < <(jq -r '.sandboxes[].name' <<< "$sandbox_json")
|
||||||
|
|
||||||
local base_sandbox_name next_version selected_sandbox
|
local base_sandbox_name next_version selected_sandbox
|
||||||
@@ -93,6 +119,8 @@ main() {
|
|||||||
local -a workspace_paths=()
|
local -a workspace_paths=()
|
||||||
|
|
||||||
if [[ "$build_from_scratch" == true ]]; then
|
if [[ "$build_from_scratch" == true ]]; then
|
||||||
|
# Keep the three name components separate so common combinations are
|
||||||
|
# quick to select while custom values remain possible.
|
||||||
prompt_component sandbox_prefix 'Sandbox prefix' 'finmas-gmbh' 'private'
|
prompt_component sandbox_prefix 'Sandbox prefix' 'finmas-gmbh' 'private'
|
||||||
prompt_component sandbox_purpose 'Sandbox purpose' 'git-work' 'generic-research'
|
prompt_component sandbox_purpose 'Sandbox purpose' 'git-work' 'generic-research'
|
||||||
prompt_component sandbox_suffix 'Sandbox suffix' 'finmas-api' 'private-api'
|
prompt_component sandbox_suffix 'Sandbox suffix' 'finmas-api' 'private-api'
|
||||||
@@ -105,18 +133,24 @@ main() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# fzf provides a searchable menu and returns a nonzero status when the
|
||||||
|
# user cancels it. Do not create anything after cancellation.
|
||||||
if ! selected_sandbox=$(printf '%s\n' "${sandbox_names[@]}" | fzf \
|
if ! selected_sandbox=$(printf '%s\n' "${sandbox_names[@]}" | fzf \
|
||||||
--prompt='Select Docker sbx image: ' --height=40% --reverse); then
|
--prompt='Select Docker sbx image: ' --height=40% --reverse); then
|
||||||
printf 'No Docker sbx image was selected.\n'
|
printf 'No Docker sbx image was selected.\n'
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Use the already captured JSON rather than querying a changing list.
|
||||||
|
# `--arg` passes the selected name as data, not as jq source code.
|
||||||
mapfile -t current_workspace_paths < <(
|
mapfile -t current_workspace_paths < <(
|
||||||
jq -r --arg name "$selected_sandbox" \
|
jq -r --arg name "$selected_sandbox" \
|
||||||
'.sandboxes[] | select(.name == $name) | .workspaces[]?' \
|
'.sandboxes[] | select(.name == $name) | .workspaces[]?' \
|
||||||
<<< "$sandbox_json"
|
<<< "$sandbox_json"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# A name ending in -v<number> is versioned. Strip that suffix and
|
||||||
|
# increment it; an unversioned source starts at version 1.
|
||||||
if [[ "$selected_sandbox" =~ ^(.*)-v([0-9]+)$ ]]; then
|
if [[ "$selected_sandbox" =~ ^(.*)-v([0-9]+)$ ]]; then
|
||||||
base_sandbox_name="${BASH_REMATCH[1]}"
|
base_sandbox_name="${BASH_REMATCH[1]}"
|
||||||
next_version=$((10#${BASH_REMATCH[2]} + 1))
|
next_version=$((10#${BASH_REMATCH[2]} + 1))
|
||||||
@@ -126,8 +160,11 @@ main() {
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Construct the final name in one place for both build modes.
|
||||||
local new_sandbox_name="${base_sandbox_name}-v${next_version}"
|
local new_sandbox_name="${base_sandbox_name}-v${next_version}"
|
||||||
|
|
||||||
|
# Arrays are important here: each workspace path remains one argument to
|
||||||
|
# `sbx create`, even when a path contains whitespace.
|
||||||
if [[ "$build_from_scratch" == true &&
|
if [[ "$build_from_scratch" == true &&
|
||||||
( "$sandbox_prefix" != 'finmas-gmbh' || "$sandbox_suffix" == 'private-api' ) ]]; then
|
( "$sandbox_prefix" != 'finmas-gmbh' || "$sandbox_suffix" == 'private-api' ) ]]; then
|
||||||
workspace_paths=("$HOME/docker-sbx/empty")
|
workspace_paths=("$HOME/docker-sbx/empty")
|
||||||
@@ -146,6 +183,7 @@ main() {
|
|||||||
printf '%s\n' "${current_workspace_paths[@]}"
|
printf '%s\n' "${current_workspace_paths[@]}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Additional paths are appended rather than replacing the default mount.
|
||||||
if ask_yes_no 'Do you want to add additional workspace directories?'; then
|
if ask_yes_no 'Do you want to add additional workspace directories?'; then
|
||||||
local workspace_path
|
local workspace_path
|
||||||
while true; do
|
while true; do
|
||||||
@@ -157,12 +195,17 @@ main() {
|
|||||||
|
|
||||||
local tmp='' api_key
|
local tmp='' api_key
|
||||||
if [[ "$build_from_scratch" == false ]]; then
|
if [[ "$build_from_scratch" == false ]]; then
|
||||||
|
# `sbx cp` needs a host-side intermediary. `mktemp` gives concurrent
|
||||||
|
# runs separate directories, and the EXIT trap removes the copy even
|
||||||
|
# if a later command fails.
|
||||||
tmp=$(mktemp -d) || {
|
tmp=$(mktemp -d) || {
|
||||||
printf 'Unable to create a temporary directory.\n' >&2
|
printf 'Unable to create a temporary directory.\n' >&2
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
trap 'rm -rf "${tmp:?}"' EXIT
|
trap 'rm -rf "${tmp:?}"' EXIT
|
||||||
|
|
||||||
|
# Copy before creating the new sandbox so a failed creation cannot
|
||||||
|
# destroy the only temporary copy of the source data.
|
||||||
if ! sbx cp "${selected_sandbox}:/home/agent/.local" "$tmp"; then
|
if ! sbx cp "${selected_sandbox}:/home/agent/.local" "$tmp"; then
|
||||||
printf 'Unable to copy /home/agent/.local from Docker sbx image: %s\n' \
|
printf 'Unable to copy /home/agent/.local from Docker sbx image: %s\n' \
|
||||||
"$selected_sandbox" >&2
|
"$selected_sandbox" >&2
|
||||||
@@ -188,12 +231,17 @@ main() {
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# `read -s` prevents the API key from being echoed. The newline afterward
|
||||||
|
# keeps subsequent terminal output readable.
|
||||||
if ! read -rsp 'HYPOPORT_API_KEY: ' api_key; then
|
if ! read -rsp 'HYPOPORT_API_KEY: ' api_key; then
|
||||||
printf '\nUnable to read the HYPOPORT_API_KEY.\n' >&2
|
printf '\nUnable to read the HYPOPORT_API_KEY.\n' >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
printf '\n'
|
printf '\n'
|
||||||
|
|
||||||
|
# Pipe the secret to `sbx exec` instead of placing it in command arguments,
|
||||||
|
# where process inspection could expose it. `%q` writes a shell-escaped
|
||||||
|
# value, and `umask 077` limits the generated file to its owner.
|
||||||
if ! printf '%s\n' "$api_key" |
|
if ! printf '%s\n' "$api_key" |
|
||||||
sbx exec "$new_sandbox_name" bash -c \
|
sbx exec "$new_sandbox_name" bash -c \
|
||||||
'umask 077; read -r api_key; printf "export HYPOPORT_API_KEY=%q\n" "$api_key" > /etc/sandbox-persistent.sh'; then
|
'umask 077; read -r api_key; printf "export HYPOPORT_API_KEY=%q\n" "$api_key" > /etc/sandbox-persistent.sh'; then
|
||||||
@@ -204,6 +252,8 @@ main() {
|
|||||||
fi
|
fi
|
||||||
unset api_key
|
unset api_key
|
||||||
|
|
||||||
|
# Cleanup is offered only after the new sandbox has been fully configured.
|
||||||
|
# An empty original inventory has nothing to display or remove.
|
||||||
if ((${#sandbox_names[@]} == 0)) || ! ask_yes_no 'Do you want to delete existing Docker sbx sandboxes?'; then
|
if ((${#sandbox_names[@]} == 0)) || ! ask_yes_no 'Do you want to delete existing Docker sbx sandboxes?'; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
@@ -221,11 +271,15 @@ main() {
|
|||||||
read -r -p 'Sandboxes to delete (for example, 1-3 or 1, 2, 7; press Enter to cancel): ' selection || return 0
|
read -r -p 'Sandboxes to delete (for example, 1-3 or 1, 2, 7; press Enter to cancel): ' selection || return 0
|
||||||
[[ -z "$selection" ]] && return 0
|
[[ -z "$selection" ]] && return 0
|
||||||
|
|
||||||
|
# An associative array is used as a set, so selecting an item twice
|
||||||
|
# still deletes it only once.
|
||||||
selected_indices=()
|
selected_indices=()
|
||||||
valid_selection=true
|
valid_selection=true
|
||||||
IFS=',' read -ra selection_parts <<< "$selection"
|
IFS=',' read -ra selection_parts <<< "$selection"
|
||||||
for selection_part in "${selection_parts[@]}"; do
|
for selection_part in "${selection_parts[@]}"; do
|
||||||
selection_part="${selection_part//[[:space:]]/}"
|
selection_part="${selection_part//[[:space:]]/}"
|
||||||
|
# Remove spaces around commas and hyphens before parsing. Validate
|
||||||
|
# every part before running the first destructive command.
|
||||||
if [[ "$selection_part" =~ ^([0-9]+)-([0-9]+)$ ]]; then
|
if [[ "$selection_part" =~ ^([0-9]+)-([0-9]+)$ ]]; then
|
||||||
range_start=$((10#${BASH_REMATCH[1]}))
|
range_start=$((10#${BASH_REMATCH[1]}))
|
||||||
range_end=$((10#${BASH_REMATCH[2]}))
|
range_end=$((10#${BASH_REMATCH[2]}))
|
||||||
@@ -233,10 +287,13 @@ main() {
|
|||||||
valid_selection=false
|
valid_selection=false
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
|
# Expand an inclusive range such as 1-3 into its menu indexes.
|
||||||
for ((index = range_start; index <= range_end; index++)); do
|
for ((index = range_start; index <= range_end; index++)); do
|
||||||
selected_indices[$index]=1
|
selected_indices[$index]=1
|
||||||
done
|
done
|
||||||
elif [[ "$selection_part" =~ ^[0-9]+$ ]]; then
|
elif [[ "$selection_part" =~ ^[0-9]+$ ]]; then
|
||||||
|
# Bash arrays are zero-based, but the displayed menu is
|
||||||
|
# intentionally one-based for people entering selections.
|
||||||
index=$((10#$selection_part))
|
index=$((10#$selection_part))
|
||||||
if ((index < 1 || index > ${#sandbox_names[@]})); then
|
if ((index < 1 || index > ${#sandbox_names[@]})); then
|
||||||
valid_selection=false
|
valid_selection=false
|
||||||
@@ -254,6 +311,8 @@ main() {
|
|||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Iterate in menu order instead of associative-array order so output
|
||||||
|
# and deletion operations are predictable.
|
||||||
for ((index = 1; index <= ${#sandbox_names[@]}; index++)); do
|
for ((index = 1; index <= ${#sandbox_names[@]}; index++)); do
|
||||||
if [[ ${selected_indices[$index]:-} == 1 ]]; then
|
if [[ ${selected_indices[$index]:-} == 1 ]]; then
|
||||||
sandbox_name="${sandbox_names[index - 1]}"
|
sandbox_name="${sandbox_names[index - 1]}"
|
||||||
|
|||||||
Reference in New Issue
Block a user