Merge pull request #4125 from adamglin0/feat/apple-container-support

feat(deploy): add Apple container support
This commit is contained in:
Wesley Liddick
2026-07-13 14:43:41 +08:00
committed by GitHub
12 changed files with 1494 additions and 9 deletions
+9
View File
@@ -8,6 +8,15 @@ permissions:
contents: read
jobs:
shell:
runs-on: macos-15
steps:
- uses: actions/checkout@v6
- name: Check deployment scripts
run: |
/bin/bash -n deploy/apple-container.sh
/bin/bash deploy/tests/apple-container-test.sh
test:
runs-on: ubuntu-latest
steps:
+2
View File
@@ -116,6 +116,8 @@ backend/.installed
# 其他
# ===================
tests
!deploy/tests/
!deploy/tests/**
CLAUDE.md
.claude
scripts
+18 -1
View File
@@ -329,6 +329,7 @@ cd sub2api/deploy
# 2. Copy environment configuration
cp .env.example .env
chmod 600 .env
# 3. Edit configuration (generate secure passwords)
nano .env
@@ -448,7 +449,23 @@ rm -rf data/ postgres_data/ redis_data/
---
### Method 3: Build from Source
### Method 3: Apple container (macOS)
Apple-silicon Macs running macOS 26 can run the full Sub2API, PostgreSQL, and Redis stack with Apple `container` 1.1.0 or newer:
```bash
git clone https://github.com/Wei-Shaw/sub2api.git
cd sub2api/deploy
./apple-container.sh init
./apple-container.sh up
./apple-container.sh status
```
This is an operator-managed local workflow; Docker Compose remains the recommended production path. See [deploy/APPLE_CONTAINER.md](deploy/APPLE_CONTAINER.md) for lifecycle commands, persistence, upgrades, and runtime limitations.
---
### Method 4: Build from Source
Build and run from source code for development or customization.
+18 -1
View File
@@ -333,6 +333,7 @@ cd sub2api/deploy
# 2. 复制环境配置文件
cp .env.example .env
chmod 600 .env
# 3. 编辑配置(生成安全密码)
nano .env
@@ -464,7 +465,23 @@ rm -rf data/ postgres_data/ redis_data/
---
### 方式三:源码编译
### 方式三:Apple container(macOS)
Apple 芯片 Mac 在 macOS 26 上可使用 Apple `container` 1.1.0 或更高版本运行完整的 Sub2API、PostgreSQL 和 Redis:
```bash
git clone https://github.com/Wei-Shaw/sub2api.git
cd sub2api/deploy
./apple-container.sh init
./apple-container.sh up
./apple-container.sh status
```
该方式面向本地开发和人工运维,不提供持续重启监管;生产部署仍推荐 Docker Compose。生命周期命令、持久化、升级和运行时限制见 [deploy/APPLE_CONTAINER.md](deploy/APPLE_CONTAINER.md)。
---
### 方式四:源码编译
从源码编译安装,适合开发或定制需求。
+18 -1
View File
@@ -327,6 +327,7 @@ cd sub2api/deploy
# 2. 環境設定ファイルをコピー
cp .env.example .env
chmod 600 .env
# 3. 設定を編集(セキュアなパスワードを生成)
nano .env
@@ -446,7 +447,23 @@ rm -rf data/ postgres_data/ redis_data/
---
### 方法3: ソースからビルド
### 方法3: Apple container(macOS)
Apple シリコン搭載 Mac と macOS 26 では、Apple `container` 1.1.0 以降を使用して Sub2API、PostgreSQL、Redis の完全なスタックを実行できます:
```bash
git clone https://github.com/Wei-Shaw/sub2api.git
cd sub2api/deploy
./apple-container.sh init
./apple-container.sh up
./apple-container.sh status
```
これはローカル開発および手動運用向けです。本番環境では引き続き Docker Compose を推奨します。ライフサイクル、永続化、アップグレード、制限については [deploy/APPLE_CONTAINER.md](deploy/APPLE_CONTAINER.md) を参照してください。
---
### 方法4: ソースからビルド
開発やカスタマイズのためにソースコードからビルドして実行します。
+13 -4
View File
@@ -1,25 +1,34 @@
# =============================================================================
# Sub2API Docker Environment Configuration
# Sub2API Container Environment Configuration
# =============================================================================
# Copy this file to .env and modify as needed:
# cp .env.example .env
# chmod 600 .env
# nano .env
#
# Then start with: docker-compose up -d
# Then start with Docker Compose or Apple container:
# docker compose up -d
# ./apple-container.sh up
# =============================================================================
# -----------------------------------------------------------------------------
# Server Configuration
# -----------------------------------------------------------------------------
# Bind address for host port mapping
# IPv4 bind address for host port mapping
BIND_HOST=0.0.0.0
# Server port (exposed on host)
# Server port exposed on the host (Apple container requires 1025-65535)
SERVER_PORT=8080
# Server mode: release or debug
SERVER_MODE=release
# Apple container image overrides (ignored by Docker Compose). Pin release tags
# or digests for repeatable operator-managed deployments.
APPLE_CONTAINER_SUB2API_IMAGE=weishaw/sub2api:latest
APPLE_CONTAINER_POSTGRES_IMAGE=postgres:18-alpine
APPLE_CONTAINER_REDIS_IMAGE=redis:8-alpine
# -----------------------------------------------------------------------------
# Logging Configuration
# 日志配置
+221
View File
@@ -0,0 +1,221 @@
# Apple container Deployment
Sub2API can run as a native three-service stack with Apple's `container` CLI. This workflow runs the published Sub2API, PostgreSQL, and Redis OCI images without Docker Desktop or a Docker-compatible daemon.
## Support Level
Apple `container` support is intended for local development and operator-managed deployments on a Mac. Docker Compose remains the recommended production deployment path.
Apple `container` 1.1 does not provide restart policies, automatic startup, workload health scheduling, a Docker API socket, or full Compose orchestration. `apple-container.sh` supplies ordered startup and readiness checks when you invoke it, but it is not a continuously running supervisor.
## Requirements
- A Mac with Apple silicon
- macOS 26 or newer
- Apple `container` 1.1.0 or newer
- `openssl` for generating initial secrets
- Local Network access for `container-runtime-linux` when macOS prompts during the first published-container startup
Install Apple `container` from its [official releases](https://github.com/apple/container/releases), then verify it:
```bash
container --version
```
## Quick Start
```bash
git clone https://github.com/Wei-Shaw/sub2api.git
cd sub2api/deploy
# Creates .env with random PostgreSQL, JWT, and TOTP secrets.
./apple-container.sh init
# Review optional settings before startup.
nano .env
# Creates volumes/network/containers, waits for dependencies, and starts Sub2API.
./apple-container.sh up
# Verifies PostgreSQL, Redis, and the application endpoint.
./apple-container.sh status
```
Open `http://localhost:8080`. If `ADMIN_PASSWORD` is empty, retrieve the generated password with:
```bash
./apple-container.sh logs app
```
The env file uses literal `KEY=value` syntax. Do not use Compose expressions such as `${VALUE:-default}`, and do not quote values unless the quote characters are part of the intended value. `BIND_HOST` must be an IPv4 address, and `SERVER_PORT` must be between 1025 and 65535.
## Commands
```bash
# Start dependencies and recreate the lightweight app container with current IPs.
./apple-container.sh up
# Also recreate PostgreSQL and Redis containers, preserving their volumes.
./apple-container.sh up --recreate
# Stop containers while preserving all resources and data.
./apple-container.sh down
# Restart PostgreSQL, Redis, and Sub2API in dependency order.
./apple-container.sh restart
# Show resource state and run live health probes.
./apple-container.sh status
# Follow one service's logs.
./apple-container.sh logs app -f
./apple-container.sh logs postgres -f
./apple-container.sh logs redis -f
# Pull all configured images for linux/arm64, then recreate containers.
./apple-container.sh pull
./apple-container.sh up --recreate
# Delete containers and the network, preserving named volumes.
./apple-container.sh destroy --yes
# Permanently delete the stack and all application/database/cache data.
./apple-container.sh destroy --volumes --yes
```
`destroy --volumes` does not remove `.env`, backup files, or pulled images. Delete credentials and backups separately when decommissioning a deployment. Use `container image delete <image>` only after confirming no other Apple containers use that image.
After a host reboot or `container system stop`, run `./apple-container.sh up` again. Apple `container` does not automatically restart persisted containers.
## Configuration
The script uses `deploy/.env`, the same source file used by Docker Compose. Export `SUB2API_ENV_FILE` to use another file for every command in the current shell:
```bash
export SUB2API_ENV_FILE=/absolute/path/to/sub2api.env
./apple-container.sh init
./apple-container.sh up
```
Apple-specific image overrides are available:
```dotenv
APPLE_CONTAINER_SUB2API_IMAGE=weishaw/sub2api:latest
APPLE_CONTAINER_POSTGRES_IMAGE=postgres:18-alpine
APPLE_CONTAINER_REDIS_IMAGE=redis:8-alpine
```
The normal `up` command recreates the application container, so application environment changes are applied immediately. Use `up --recreate` when changing PostgreSQL or Redis container images or Redis runtime configuration. Persistent data remains in named volumes.
`POSTGRES_USER`, `POSTGRES_PASSWORD`, and `POSTGRES_DB` are applied only when PostgreSQL initializes an empty data volume. Changing them in `.env` and recreating the container does not change an existing database. Rotate a password with `ALTER ROLE`, and plan explicit migrations for user or database changes. To intentionally initialize a new empty database, first back up the old one and use `destroy --volumes`.
Apple-specific handling of shared settings:
| Setting | Apple workflow behavior |
|---|---|
| Application and gateway variables | Passed to Sub2API from `.env` |
| `BIND_HOST`, `SERVER_PORT` | Used for the macOS published port |
| `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB` | PostgreSQL first initialization only |
| `REDIS_PASSWORD` | Applied to Redis and Sub2API |
| `DATABASE_PORT`, `REDIS_PORT` | Internal ports are fixed to 5432 and 6379 |
| `POSTGRES_MAX_*`, `REDIS_MAXCLIENTS` | Not currently applied to the database/cache server |
## Managed Resources
The script creates only resources carrying the `org.sub2api.stack=apple-container` label:
| Type | Names |
|---|---|
| Containers | `sub2api-apple`, `sub2api-apple-postgres`, `sub2api-apple-redis` |
| Network | `sub2api-apple` |
| Volumes | `sub2api-apple-data`, `sub2api-apple-postgres-data`, `sub2api-apple-redis-data` |
The PostgreSQL volume is mounted at `/var/lib/postgresql`, retaining PostgreSQL 18's default child data directory. Sub2API and Redis also store data in child directories below their Apple volume mount points. This is required because Apple named volumes do not have Docker's copy-up and mount-point ownership behavior.
## Networking
Apple `container` 1.1 does not provide Compose-style network-scoped service aliases. After PostgreSQL and Redis start, the script reads their current private-network IPv4 addresses from `container inspect`, injects those addresses into a newly created application container, and then starts Sub2API. The script does not modify `~/.config/container/config.toml` or the macOS host resolver.
All three services attach only to the private `sub2api-apple` network. Only the application publishes a host port; database and Redis ports remain unpublished.
The application container is intentionally recreated by every `up` and `restart` operation because dependency VM addresses can change after they stop. Application data remains in `sub2api-apple-data`.
The script checks the published `/health` endpoint from macOS before reporting success. Approve the Local Network prompt on first startup. If the internal probe succeeds but the host-port probe fails with a connection reset, enable Local Network access for `container-runtime-linux`, run `container system stop` followed by `container system start`, and then run `up` again. Runtime upgrades may prompt for permission again.
## Backup and Upgrade
Pin image release tags or digests in `.env` before using this workflow for persistent data. Before an application or database image upgrade, create backups while the stack is healthy:
```bash
umask 077
mkdir -p backups
# Logical PostgreSQL backup.
container exec sub2api-apple sh -c \
'PGPASSWORD="$DATABASE_PASSWORD" pg_dump -h "$DATABASE_HOST" -U "$DATABASE_USER" "$DATABASE_DBNAME"' \
> backups/sub2api.sql
# Application configuration and local files.
container exec sub2api-apple sh -c 'tar -C "$DATA_DIR" -czf - .' \
> backups/sub2api-data.tar.gz
./apple-container.sh pull
./apple-container.sh up --recreate
./apple-container.sh status
```
Database migrations are forward-only. Keep the previous image reference and both backups until the upgraded stack has been validated; image rollback alone cannot reverse a migrated database. Test restore procedures before relying on this workflow for important data.
To restore these backups into an existing stack, first ensure the image versions are compatible with the backup, then stop writers and replace both data sets:
```bash
# Ensure empty/current resources exist, then stop the stack.
./apple-container.sh up
./apple-container.sh down
# Remove only the app container so a helper can mount its named volume.
container delete sub2api-apple
SUB2API_IMAGE=weishaw/sub2api:latest # Match APPLE_CONTAINER_SUB2API_IMAGE in .env.
container run --rm --name sub2api-apple-data-restore \
--entrypoint /bin/sh \
--volume sub2api-apple-data:/restore \
--volume "$PWD/backups:/backup:ro" \
"$SUB2API_IMAGE" \
-c 'rm -rf /restore/data && mkdir -p /restore/data && tar -xzf /backup/sub2api-data.tar.gz -C /restore/data'
# Restore the logical database while the application is absent.
container start sub2api-apple-postgres
until container exec sub2api-apple-postgres sh -c 'pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB"'; do sleep 1; done
container copy backups/sub2api.sql sub2api-apple-postgres:/tmp/sub2api.sql
container exec sub2api-apple-postgres sh -c '
export PGPASSWORD="$POSTGRES_PASSWORD"
dropdb -h 127.0.0.1 -U "$POSTGRES_USER" --if-exists --force "$POSTGRES_DB"
createdb -h 127.0.0.1 -U "$POSTGRES_USER" "$POSTGRES_DB"
psql -h 127.0.0.1 -U "$POSTGRES_USER" -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -f /tmp/sub2api.sql
rm /tmp/sub2api.sql
'
./apple-container.sh up
./apple-container.sh status
```
For disaster recovery after deleting the named volumes, run `up` once to create a fresh stack before following the restore sequence. Perform restore drills with non-production data first.
To upgrade the Apple runtime itself:
```bash
./apple-container.sh down
container system stop
# Install/update Apple container 1.1.0 or newer.
container system start
./apple-container.sh up
```
## Operational Limitations
- There is no `restart: unless-stopped` equivalent. Run `up` after reboot, or add your own launchd supervisor.
- Health probes run during `up`, `restart`, and `status`; Apple `container` does not continuously schedule them.
- Docker Compose, Testcontainers, Buildx, and tools requiring `/var/run/docker.sock` cannot use this runtime directly.
- Named volume backup and restore must be tested before using this workflow for important data.
- The script targets native `linux/arm64` images. The normal Sub2API release publishes an arm64 variant.
- Runtime environment values, including credentials, are retained in Apple container configuration and are visible to users who can inspect the local runtime.
+23 -2
View File
@@ -1,12 +1,13 @@
# Sub2API Deployment Files
This directory contains files for deploying Sub2API on Linux servers.
This directory contains files for deploying Sub2API on Linux servers and Apple-silicon Macs.
## Deployment Methods
| Method | Best For | Setup Wizard |
|--------|----------|--------------|
| **Docker Compose** | Quick setup, all-in-one | Not needed (auto-setup) |
| **Apple container** | Native local stack on macOS 26 | Not needed (auto-setup) |
| **Binary Install** | Production servers, systemd | Web-based wizard |
## Files
@@ -16,7 +17,9 @@ This directory contains files for deploying Sub2API on Linux servers.
| `docker-compose.yml` | Docker Compose configuration (named volumes) |
| `docker-compose.local.yml` | Docker Compose configuration (local directories, easy migration) |
| `docker-deploy.sh` | **One-click Docker deployment script (recommended)** |
| `.env.example` | Docker environment variables template |
| `apple-container.sh` | Native Apple `container` lifecycle script |
| `APPLE_CONTAINER.md` | Apple `container` deployment and operations guide |
| `.env.example` | Container environment variables template |
| `DOCKER.md` | Docker Hub documentation |
| `install.sh` | One-click binary installation script |
| `install-datamanagementd.sh` | datamanagementd 一键安装脚本 |
@@ -27,6 +30,23 @@ This directory contains files for deploying Sub2API on Linux servers.
---
## Apple container Deployment
Apple-silicon Macs running macOS 26 can run the complete Sub2API, PostgreSQL, and Redis stack with Apple `container` 1.1.0 or newer:
```bash
./apple-container.sh init
./apple-container.sh up
./apple-container.sh status
./apple-container.sh logs app -f
```
The script uses Apple named volumes, starts dependencies in order, and performs live readiness checks. It does not provide a continuous restart supervisor; run `./apple-container.sh up` after a host reboot. Docker Compose remains the recommended production deployment path.
See [APPLE_CONTAINER.md](./APPLE_CONTAINER.md) for configuration, upgrades, persistence, networking behavior, and limitations.
---
## Docker Deployment (Recommended)
### Method 1: One-Click Deployment (Recommended)
@@ -76,6 +96,7 @@ cd sub2api/deploy
# Configure environment
cp .env.example .env
chmod 600 .env
nano .env # Set POSTGRES_PASSWORD and other required variables
# Generate secure secrets (recommended)
+926
View File
@@ -0,0 +1,926 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ENV_FILE="${SUB2API_ENV_FILE:-${SCRIPT_DIR}/.env}"
STACK_LABEL_KEY="org.sub2api.stack"
STACK_LABEL_VALUE="apple-container"
NETWORK_NAME="sub2api-apple"
APP_CONTAINER="sub2api-apple"
POSTGRES_CONTAINER="sub2api-apple-postgres"
REDIS_CONTAINER="sub2api-apple-redis"
APP_VOLUME="sub2api-apple-data"
POSTGRES_VOLUME="sub2api-apple-postgres-data"
REDIS_VOLUME="sub2api-apple-redis-data"
PLATFORM="linux/arm64"
TEMP_DIR=""
LOCK_DIR="${TMPDIR:-/tmp}/sub2api-apple-container.lock"
LOCK_ACQUIRED=false
APP_IMAGE=""
POSTGRES_IMAGE=""
REDIS_IMAGE=""
BIND_HOST=""
HOST_PORT=""
ACCESS_HOST=""
POSTGRES_USER=""
POSTGRES_PASSWORD=""
POSTGRES_DB=""
REDIS_PASSWORD=""
TZ_VALUE=""
POSTGRES_ADDRESS=""
REDIS_ADDRESS=""
APP_ENV_FILE=""
POSTGRES_ENV_FILE=""
POSTGRES_PROBE_ENV_FILE=""
REDIS_ENV_FILE=""
info() {
printf '[INFO] %s\n' "$*"
}
warn() {
printf '[WARN] %s\n' "$*" >&2
}
die() {
printf '[ERROR] %s\n' "$*" >&2
exit 1
}
usage() {
cat <<'EOF'
Usage: ./apple-container.sh <command> [options]
Commands:
init Create .env and generate required secrets
up [--recreate] Create and start the complete Sub2API stack
down Stop the stack and preserve all data
restart Restart the stack in dependency order
status Show container and workload health
logs <service> [-f] Show logs for app, postgres, or redis
pull Pull all stack images for linux/arm64
destroy [options] Delete stack containers and network
Destroy options:
--volumes Also delete all persistent data volumes
--yes Skip the confirmation prompt
Environment:
SUB2API_ENV_FILE Path to the deployment env file (default: deploy/.env)
EOF
}
cleanup() {
local exit_code=$?
if [[ -n "${TEMP_DIR}" && -d "${TEMP_DIR}" ]]; then
rm -rf "${TEMP_DIR}"
fi
if [[ "${LOCK_ACQUIRED}" == true && -d "${LOCK_DIR}" ]]; then
rm -f "${LOCK_DIR}/pid"
rmdir "${LOCK_DIR}" 2>/dev/null || true
fi
exit "${exit_code}"
}
acquire_lock() {
if ! mkdir "${LOCK_DIR}" 2>/dev/null; then
local owner_pid=""
if [[ -f "${LOCK_DIR}/pid" ]]; then
owner_pid="$(<"${LOCK_DIR}/pid")"
fi
if [[ "${owner_pid}" =~ ^[0-9]+$ ]] && ! kill -0 "${owner_pid}" 2>/dev/null; then
rm -rf "${LOCK_DIR}"
mkdir "${LOCK_DIR}" || die "Failed to reclaim stale operation lock."
else
die "Another Sub2API Apple container operation is already running."
fi
fi
printf '%s\n' "$$" >"${LOCK_DIR}/pid"
LOCK_ACQUIRED=true
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
trap 'exit 129' HUP
}
require_command() {
command -v "$1" >/dev/null 2>&1 || die "Required command not found: $1"
}
require_container_version() {
local version_output major minor
require_command container
require_command plutil
version_output="$(container --version)"
if [[ ! "${version_output}" =~ ([0-9]+)\.([0-9]+)\.([0-9]+) ]]; then
die "Unable to parse Apple container version: ${version_output}"
fi
major="${BASH_REMATCH[1]}"
minor="${BASH_REMATCH[2]}"
if (( major < 1 || (major == 1 && minor < 1) )); then
die "Apple container 1.1.0 or newer is required; found ${version_output}."
fi
}
system_is_running() {
container system status >/dev/null 2>&1
}
start_system() {
if ! system_is_running; then
info "Starting Apple container services..."
container system start --enable-kernel-install
fi
}
list_resource_ids() {
case "$1" in
container) container list --all --quiet ;;
network) container network list --quiet ;;
volume) container volume list --quiet ;;
*) die "Unknown resource type: $1" ;;
esac
}
resource_exists() {
local resource_type=$1
local resource_name=$2
local output line
if ! output="$(list_resource_ids "${resource_type}")"; then
die "Failed to list Apple container ${resource_type} resources."
fi
while IFS= read -r line; do
if [[ "${line}" == "${resource_name}" ]]; then
return 0
fi
done <<<"${output}"
return 1
}
inspect_resource() {
case "$1" in
container) container inspect "$2" ;;
network) container network inspect "$2" ;;
volume) container volume inspect "$2" ;;
*) die "Unknown resource type: $1" ;;
esac
}
assert_resource_owned() {
local resource_type=$1
local resource_name=$2
local inspection compact
inspection="$(inspect_resource "${resource_type}" "${resource_name}" | \
plutil -extract 0.configuration.labels json -o - -)" || \
die "Failed to inspect ${resource_type} ${resource_name}."
compact="$(printf '%s' "${inspection}" | tr -d '[:space:]')"
if [[ "${compact}" != *"\"${STACK_LABEL_KEY}\":\"${STACK_LABEL_VALUE}\""* ]]; then
die "Refusing to manage existing ${resource_type} '${resource_name}' because it is not owned by this stack."
fi
}
preflight_stack_ownership() {
local resource_name
for resource_name in "${APP_CONTAINER}" "${REDIS_CONTAINER}" "${POSTGRES_CONTAINER}"; do
if resource_exists container "${resource_name}"; then
assert_resource_owned container "${resource_name}"
fi
done
if resource_exists network "${NETWORK_NAME}"; then
assert_resource_owned network "${NETWORK_NAME}"
fi
for resource_name in "${APP_VOLUME}" "${REDIS_VOLUME}" "${POSTGRES_VOLUME}"; do
if resource_exists volume "${resource_name}"; then
assert_resource_owned volume "${resource_name}"
fi
done
}
ensure_network() {
if resource_exists network "${NETWORK_NAME}"; then
assert_resource_owned network "${NETWORK_NAME}"
return
fi
info "Creating network ${NETWORK_NAME}..."
container network create \
--label "${STACK_LABEL_KEY}=${STACK_LABEL_VALUE}" \
"${NETWORK_NAME}" >/dev/null
}
ensure_volume() {
local volume_name=$1
if resource_exists volume "${volume_name}"; then
assert_resource_owned volume "${volume_name}"
return
fi
info "Creating volume ${volume_name}..."
container volume create \
--label "${STACK_LABEL_KEY}=${STACK_LABEL_VALUE}" \
"${volume_name}" >/dev/null
}
ensure_image_available() {
local image=$1
if container image inspect "${image}" >/dev/null 2>&1; then
return
fi
info "Pulling ${image}..."
container image pull --platform "${PLATFORM}" "${image}"
}
container_is_running() {
local container_name=$1
local output line
output="$(container list --quiet)" || die "Failed to list running Apple containers."
while IFS= read -r line; do
if [[ "${line}" == "${container_name}" ]]; then
return 0
fi
done <<<"${output}"
return 1
}
ensure_system() {
require_container_version
require_command curl
start_system
}
container_ipv4_address() {
local container_name=$1
local address
address="$(container inspect "${container_name}" | \
plutil -extract 0.status.networks.0.ipv4Address raw -o - -)" || \
die "Unable to read the network address for ${container_name}."
address="${address%%/*}"
[[ "${address}" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]] || \
die "Apple container returned an invalid IPv4 address for ${container_name}: ${address}"
printf '%s\n' "${address}"
}
read_env_value() {
local key=$1
local fallback=${2-}
awk -v wanted="${key}" -v fallback="${fallback}" '
BEGIN { found = 0 }
/^[[:space:]]*#/ || /^[[:space:]]*$/ { next }
{
separator = index($0, "=")
if (separator == 0) { next }
key = substr($0, 1, separator - 1)
if (key == wanted) {
value = substr($0, separator + 1)
sub(/\r$/, "", value)
found = 1
}
}
END {
if (found) { print value }
else { print fallback }
}
' "${ENV_FILE}"
}
replace_env_value() {
local key=$1
local value=$2
local target_file=${3:-${ENV_FILE}}
local temp_file="${target_file}.tmp.$$"
awk -v wanted="${key}" -v replacement="${value}" '
BEGIN { replaced = 0 }
{
separator = index($0, "=")
key = separator == 0 ? "" : substr($0, 1, separator - 1)
if (key == wanted) {
if (!replaced) { print wanted "=" replacement }
replaced = 1
next
}
print
}
END {
if (!replaced) { print wanted "=" replacement }
}
' "${target_file}" >"${temp_file}"
chmod 600 "${temp_file}"
mv "${temp_file}" "${target_file}"
}
generate_secret() {
openssl rand -hex 32
}
cmd_init() {
local env_dir temp_file postgres_secret jwt_secret totp_secret
require_command openssl
if [[ -e "${ENV_FILE}" ]]; then
die "Environment file already exists: ${ENV_FILE}"
fi
postgres_secret="$(generate_secret)" || die "Failed to generate PostgreSQL password."
jwt_secret="$(generate_secret)" || die "Failed to generate JWT secret."
totp_secret="$(generate_secret)" || die "Failed to generate TOTP encryption key."
[[ -n "${postgres_secret}" && -n "${jwt_secret}" && -n "${totp_secret}" ]] || \
die "Secret generation returned an empty value."
env_dir="$(dirname "${ENV_FILE}")"
temp_file="${ENV_FILE}.init.tmp.$$"
mkdir -p "${env_dir}"
cp "${SCRIPT_DIR}/.env.example" "${temp_file}"
chmod 600 "${temp_file}"
replace_env_value POSTGRES_PASSWORD "${postgres_secret}" "${temp_file}"
replace_env_value JWT_SECRET "${jwt_secret}" "${temp_file}"
replace_env_value TOTP_ENCRYPTION_KEY "${totp_secret}" "${temp_file}"
mv "${temp_file}" "${ENV_FILE}"
info "Created ${ENV_FILE} with generated secrets."
info "Review the file, then run: SUB2API_ENV_FILE='${ENV_FILE}' ${SCRIPT_DIR}/apple-container.sh up"
}
validate_port() {
local port=$1
local decimal_port
[[ "${port}" =~ ^[0-9]+$ ]] || die "SERVER_PORT must be numeric: ${port}"
decimal_port=$((10#${port}))
(( decimal_port >= 1025 && decimal_port <= 65535 )) || \
die "SERVER_PORT must be between 1025 and 65535 for Apple container port forwarding."
}
validate_ipv4_address() {
local address=$1
local first second third fourth extra octet
IFS=. read -r first second third fourth extra <<<"${address}"
[[ -n "${first}" && -n "${second}" && -n "${third}" && -n "${fourth}" && -z "${extra}" ]] || \
die "BIND_HOST must be a valid IPv4 address: ${address}"
for octet in "${first}" "${second}" "${third}" "${fourth}"; do
[[ "${octet}" =~ ^[0-9]+$ ]] || die "BIND_HOST must be a valid IPv4 address: ${address}"
(( 10#${octet} <= 255 )) || die "BIND_HOST must be a valid IPv4 address: ${address}"
done
}
validate_env_file_security() {
local owner mode permissions
[[ -f "${ENV_FILE}" ]] || die "Environment file not found: ${ENV_FILE}. Run '$0 init' first."
owner="$(stat -f '%u' "${ENV_FILE}")" || die "Unable to read owner for ${ENV_FILE}."
mode="$(stat -f '%Lp' "${ENV_FILE}")" || die "Unable to read permissions for ${ENV_FILE}."
[[ "${owner}" == "${EUID}" ]] || die "Environment file must be owned by the current user: ${ENV_FILE}"
[[ "${mode}" =~ ^[0-7]+$ ]] || die "Unable to parse permissions for ${ENV_FILE}: ${mode}"
permissions=$((8#${mode}))
(( (permissions & 077) == 0 )) || \
die "Environment file must not be readable by group or others. Run: chmod 600 '${ENV_FILE}'"
}
prepare_environment() {
validate_env_file_security
APP_IMAGE="$(read_env_value APPLE_CONTAINER_SUB2API_IMAGE weishaw/sub2api:latest)"
POSTGRES_IMAGE="$(read_env_value APPLE_CONTAINER_POSTGRES_IMAGE postgres:18-alpine)"
REDIS_IMAGE="$(read_env_value APPLE_CONTAINER_REDIS_IMAGE redis:8-alpine)"
BIND_HOST="$(read_env_value BIND_HOST 0.0.0.0)"
HOST_PORT="$(read_env_value SERVER_PORT 8080)"
POSTGRES_USER="$(read_env_value POSTGRES_USER sub2api)"
POSTGRES_PASSWORD="$(read_env_value POSTGRES_PASSWORD)"
POSTGRES_DB="$(read_env_value POSTGRES_DB sub2api)"
REDIS_PASSWORD="$(read_env_value REDIS_PASSWORD)"
TZ_VALUE="$(read_env_value TZ Asia/Shanghai)"
[[ -n "${BIND_HOST}" ]] || die "BIND_HOST must not be empty."
validate_ipv4_address "${BIND_HOST}"
validate_port "${HOST_PORT}"
if [[ "${BIND_HOST}" == "0.0.0.0" ]]; then
ACCESS_HOST="127.0.0.1"
else
ACCESS_HOST="${BIND_HOST}"
fi
[[ -n "${POSTGRES_USER}" ]] || die "POSTGRES_USER must not be empty."
[[ -n "${POSTGRES_DB}" ]] || die "POSTGRES_DB must not be empty."
if [[ -z "${POSTGRES_PASSWORD}" || "${POSTGRES_PASSWORD}" == "change_this_secure_password" ]]; then
die "Set a secure POSTGRES_PASSWORD in ${ENV_FILE}."
fi
TEMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/sub2api-apple.XXXXXX")"
APP_ENV_FILE="${TEMP_DIR}/app.env"
POSTGRES_ENV_FILE="${TEMP_DIR}/postgres.env"
POSTGRES_PROBE_ENV_FILE="${TEMP_DIR}/postgres-probe.env"
REDIS_ENV_FILE="${TEMP_DIR}/redis.env"
cat >"${POSTGRES_ENV_FILE}" <<EOF
POSTGRES_USER=${POSTGRES_USER}
POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
POSTGRES_DB=${POSTGRES_DB}
TZ=${TZ_VALUE}
EOF
cat >"${POSTGRES_PROBE_ENV_FILE}" <<EOF
PGPASSWORD=${POSTGRES_PASSWORD}
EOF
cat >"${REDIS_ENV_FILE}" <<EOF
REDIS_PASSWORD=${REDIS_PASSWORD}
TZ=${TZ_VALUE}
EOF
if [[ -n "${REDIS_PASSWORD}" ]]; then
printf 'REDISCLI_AUTH=%s\n' "${REDIS_PASSWORD}" >>"${REDIS_ENV_FILE}"
fi
chmod 600 "${POSTGRES_ENV_FILE}" "${POSTGRES_PROBE_ENV_FILE}" "${REDIS_ENV_FILE}"
}
prepare_app_environment() {
[[ -n "${POSTGRES_ADDRESS}" && -n "${REDIS_ADDRESS}" ]] || \
die "Dependency network addresses are not available."
cp "${ENV_FILE}" "${APP_ENV_FILE}"
cat >>"${APP_ENV_FILE}" <<EOF
AUTO_SETUP=true
SERVER_HOST=0.0.0.0
SERVER_PORT=8080
DATABASE_HOST=${POSTGRES_ADDRESS}
DATABASE_PORT=5432
DATABASE_USER=${POSTGRES_USER}
DATABASE_PASSWORD=${POSTGRES_PASSWORD}
DATABASE_DBNAME=${POSTGRES_DB}
DATABASE_SSLMODE=disable
REDIS_HOST=${REDIS_ADDRESS}
REDIS_PORT=6379
REDIS_PASSWORD=${REDIS_PASSWORD}
DATA_DIR=/app/storage/data
EOF
chmod 600 "${APP_ENV_FILE}"
}
create_postgres_container() {
info "Creating PostgreSQL container..."
container create \
--name "${POSTGRES_CONTAINER}" \
--label "${STACK_LABEL_KEY}=${STACK_LABEL_VALUE}" \
--network "${NETWORK_NAME}" \
--platform "${PLATFORM}" \
--ulimit nofile=100000:100000 \
--env-file "${POSTGRES_ENV_FILE}" \
--volume "${POSTGRES_VOLUME}:/var/lib/postgresql" \
"${POSTGRES_IMAGE}" >/dev/null
}
create_redis_container() {
info "Creating Redis container..."
container create \
--name "${REDIS_CONTAINER}" \
--label "${STACK_LABEL_KEY}=${STACK_LABEL_VALUE}" \
--network "${NETWORK_NAME}" \
--platform "${PLATFORM}" \
--ulimit nofile=100000:100000 \
--env-file "${REDIS_ENV_FILE}" \
--volume "${REDIS_VOLUME}:/var/lib/redis" \
"${REDIS_IMAGE}" \
sh -c 'set -e; mkdir -p /var/lib/redis/data; chown redis:redis /var/lib/redis/data; exec /usr/local/bin/docker-entrypoint.sh redis-server --dir /var/lib/redis/data --save 60 1 --appendonly yes --appendfsync everysec ${REDIS_PASSWORD:+--requirepass "$REDIS_PASSWORD"}' \
>/dev/null
}
create_app_container() {
info "Creating Sub2API container..."
container create \
--name "${APP_CONTAINER}" \
--label "${STACK_LABEL_KEY}=${STACK_LABEL_VALUE}" \
--network "${NETWORK_NAME}" \
--platform "${PLATFORM}" \
--ulimit nofile=100000:100000 \
--publish "${BIND_HOST}:${HOST_PORT}:8080/tcp" \
--env-file "${APP_ENV_FILE}" \
--volume "${APP_VOLUME}:/app/storage" \
--entrypoint /bin/sh \
"${APP_IMAGE}" \
-c 'set -e; mkdir -p "$DATA_DIR"; chown -R sub2api:sub2api "$DATA_DIR"; exec su-exec sub2api /app/sub2api' \
>/dev/null
}
ensure_container() {
local container_name=$1
local create_function=$2
if resource_exists container "${container_name}"; then
assert_resource_owned container "${container_name}"
return
fi
"${create_function}"
}
start_container_if_needed() {
local container_name=$1
if container_is_running "${container_name}"; then
return
fi
info "Starting ${container_name}..."
container start "${container_name}" >/dev/null
}
stop_container_if_running() {
local container_name=$1
if ! resource_exists container "${container_name}"; then
return
fi
assert_resource_owned container "${container_name}"
if container_is_running "${container_name}"; then
info "Stopping ${container_name}..."
container stop --time 30 "${container_name}" >/dev/null
fi
}
delete_container_if_present() {
local container_name=$1
if ! resource_exists container "${container_name}"; then
return
fi
assert_resource_owned container "${container_name}"
if container_is_running "${container_name}"; then
container stop --time 30 "${container_name}" >/dev/null
fi
info "Deleting ${container_name}..."
container delete "${container_name}" >/dev/null
}
wait_for_probe() {
local description=$1
local attempts=$2
shift 2
local attempt
for ((attempt = 1; attempt <= attempts; attempt++)); do
if "$@" >/dev/null 2>&1; then
info "${description} is ready."
return 0
fi
sleep 1
done
return 1
}
probe_postgres() {
container exec --env-file "${POSTGRES_PROBE_ENV_FILE}" \
"${POSTGRES_CONTAINER}" \
psql -h 127.0.0.1 -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" \
-v ON_ERROR_STOP=1 -tAc 'SELECT 1'
}
probe_redis() {
container exec --env-file "${REDIS_ENV_FILE}" \
"${REDIS_CONTAINER}" \
redis-cli ping
}
probe_app() {
container exec "${APP_CONTAINER}" \
wget -q -T 5 -O /dev/null http://localhost:8080/health
}
probe_host_app() {
curl --fail --silent --show-error --max-time 5 \
"http://${ACCESS_HOST}:${HOST_PORT}/health"
}
show_failure_logs() {
local container_name=$1
warn "Last logs from ${container_name}:"
container logs -n 50 "${container_name}" >&2 || true
}
start_dependencies() {
start_container_if_needed "${POSTGRES_CONTAINER}"
if ! wait_for_probe "PostgreSQL" 90 probe_postgres; then
show_failure_logs "${POSTGRES_CONTAINER}"
die "PostgreSQL did not become ready."
fi
start_container_if_needed "${REDIS_CONTAINER}"
if ! wait_for_probe "Redis" 60 probe_redis; then
show_failure_logs "${REDIS_CONTAINER}"
die "Redis did not become ready."
fi
}
start_app() {
start_container_if_needed "${APP_CONTAINER}"
if ! wait_for_probe "Sub2API" 180 probe_app; then
show_failure_logs "${APP_CONTAINER}"
die "Sub2API did not become ready."
fi
if ! wait_for_probe "Sub2API host port" 15 probe_host_app; then
die "Host port forwarding failed. In System Settings > Privacy & Security > Local Network, allow container-runtime-linux; restart Apple container services; then run 'apple-container.sh up' again."
fi
}
cmd_up() {
local recreate=false
if [[ $# -gt 1 || ($# -eq 1 && "${1-}" != "--recreate") ]]; then
usage
exit 2
fi
if [[ $# -eq 1 ]]; then
recreate=true
fi
ensure_system
prepare_environment
preflight_stack_ownership
ensure_network
ensure_volume "${APP_VOLUME}"
ensure_volume "${POSTGRES_VOLUME}"
ensure_volume "${REDIS_VOLUME}"
ensure_image_available "${APP_IMAGE}"
ensure_image_available "${POSTGRES_IMAGE}"
ensure_image_available "${REDIS_IMAGE}"
if [[ "${recreate}" == true ]]; then
delete_container_if_present "${APP_CONTAINER}"
delete_container_if_present "${REDIS_CONTAINER}"
delete_container_if_present "${POSTGRES_CONTAINER}"
fi
ensure_container "${POSTGRES_CONTAINER}" create_postgres_container
ensure_container "${REDIS_CONTAINER}" create_redis_container
start_dependencies
POSTGRES_ADDRESS="$(container_ipv4_address "${POSTGRES_CONTAINER}")"
REDIS_ADDRESS="$(container_ipv4_address "${REDIS_CONTAINER}")"
prepare_app_environment
# The dependency IPs may change whenever their lightweight VMs restart.
delete_container_if_present "${APP_CONTAINER}"
create_app_container
start_app
info "Sub2API is available at http://${ACCESS_HOST}:${HOST_PORT}"
}
cmd_down() {
require_container_version
if ! system_is_running; then
info "Apple container services are already stopped."
return
fi
preflight_stack_ownership
stop_container_if_running "${APP_CONTAINER}"
stop_container_if_running "${REDIS_CONTAINER}"
stop_container_if_running "${POSTGRES_CONTAINER}"
info "Sub2API stack stopped; persistent volumes were preserved."
}
cmd_restart() {
cmd_down
cmd_up
}
print_container_status() {
local service=$1
local container_name=$2
if ! resource_exists container "${container_name}"; then
printf '%-12s %s\n' "${service}" "missing"
elif container_is_running "${container_name}"; then
printf '%-12s %s\n' "${service}" "running"
else
printf '%-12s %s\n' "${service}" "stopped"
fi
}
cmd_status() {
local failed=0
require_container_version
if ! system_is_running; then
printf '%-12s %s\n' "system" "stopped"
return 1
fi
printf '%-12s %s\n' "system" "running"
preflight_stack_ownership
print_container_status app "${APP_CONTAINER}"
print_container_status postgres "${POSTGRES_CONTAINER}"
print_container_status redis "${REDIS_CONTAINER}"
if [[ -f "${ENV_FILE}" ]]; then
prepare_environment
if container_is_running "${POSTGRES_CONTAINER}" && probe_postgres >/dev/null 2>&1; then
printf '%-12s %s\n' "postgres" "healthy"
else
printf '%-12s %s\n' "postgres" "unhealthy"
failed=1
fi
if container_is_running "${REDIS_CONTAINER}" && probe_redis >/dev/null 2>&1; then
printf '%-12s %s\n' "redis" "healthy"
else
printf '%-12s %s\n' "redis" "unhealthy"
failed=1
fi
if container_is_running "${APP_CONTAINER}" && probe_app >/dev/null 2>&1; then
printf '%-12s %s\n' "app" "healthy"
else
printf '%-12s %s\n' "app" "unhealthy"
failed=1
fi
if container_is_running "${APP_CONTAINER}" && probe_host_app >/dev/null 2>&1; then
printf '%-12s %s\n' "host-port" "healthy"
else
printf '%-12s %s\n' "host-port" "unhealthy"
failed=1
fi
else
warn "Health probes require ${ENV_FILE}."
failed=1
fi
return "${failed}"
}
cmd_logs() {
local service=${1-}
local follow=${2-}
local container_name
[[ $# -ge 1 && $# -le 2 ]] || { usage; exit 2; }
if [[ -n "${follow}" && "${follow}" != "-f" && "${follow}" != "--follow" ]]; then
usage
exit 2
fi
case "${service}" in
app|sub2api) container_name="${APP_CONTAINER}" ;;
postgres) container_name="${POSTGRES_CONTAINER}" ;;
redis) container_name="${REDIS_CONTAINER}" ;;
*) die "Unknown service '${service}'. Use app, postgres, or redis." ;;
esac
require_container_version
system_is_running || die "Apple container services are not running."
resource_exists container "${container_name}" || die "Container not found: ${container_name}"
assert_resource_owned container "${container_name}"
if [[ -n "${follow}" ]]; then
container logs --follow "${container_name}"
else
container logs "${container_name}"
fi
}
cmd_pull() {
ensure_system
prepare_environment
info "Pulling ${APP_IMAGE}..."
container image pull --platform "${PLATFORM}" "${APP_IMAGE}"
info "Pulling ${POSTGRES_IMAGE}..."
container image pull --platform "${PLATFORM}" "${POSTGRES_IMAGE}"
info "Pulling ${REDIS_IMAGE}..."
container image pull --platform "${PLATFORM}" "${REDIS_IMAGE}"
}
confirm_destroy() {
local include_volumes=$1
local answer
if [[ "${include_volumes}" == true ]]; then
printf 'Delete the Sub2API stack and all persistent data? [y/N] '
else
printf 'Delete the Sub2API containers and network, preserving volumes? [y/N] '
fi
read -r answer
[[ "${answer}" == "y" || "${answer}" == "Y" ]]
}
delete_volume_if_present() {
local volume_name=$1
if resource_exists volume "${volume_name}"; then
assert_resource_owned volume "${volume_name}"
info "Deleting volume ${volume_name}..."
container volume delete "${volume_name}" >/dev/null
fi
}
cmd_destroy() {
local include_volumes=false
local assume_yes=false
local argument
for argument in "$@"; do
case "${argument}" in
--volumes) include_volumes=true ;;
--yes) assume_yes=true ;;
*) usage; exit 2 ;;
esac
done
require_container_version
start_system
preflight_stack_ownership
if [[ "${assume_yes}" != true ]] && ! confirm_destroy "${include_volumes}"; then
info "Cancelled."
return
fi
delete_container_if_present "${APP_CONTAINER}"
delete_container_if_present "${REDIS_CONTAINER}"
delete_container_if_present "${POSTGRES_CONTAINER}"
if resource_exists network "${NETWORK_NAME}"; then
assert_resource_owned network "${NETWORK_NAME}"
info "Deleting network ${NETWORK_NAME}..."
container network delete "${NETWORK_NAME}" >/dev/null
fi
if [[ "${include_volumes}" == true ]]; then
delete_volume_if_present "${APP_VOLUME}"
delete_volume_if_present "${REDIS_VOLUME}"
delete_volume_if_present "${POSTGRES_VOLUME}"
info "Sub2API stack and persistent data deleted."
else
info "Sub2API stack deleted; persistent volumes were preserved."
fi
}
main() {
local command=${1-}
if [[ $# -gt 0 ]]; then
shift
fi
case "${command}" in
init)
[[ $# -eq 0 ]] || { usage; exit 2; }
acquire_lock
cmd_init
;;
up)
acquire_lock
cmd_up "$@"
;;
down)
[[ $# -eq 0 ]] || { usage; exit 2; }
acquire_lock
cmd_down
;;
restart)
[[ $# -eq 0 ]] || { usage; exit 2; }
acquire_lock
cmd_restart
;;
status)
[[ $# -eq 0 ]] || { usage; exit 2; }
trap cleanup EXIT
cmd_status
;;
logs)
cmd_logs "$@"
;;
pull)
[[ $# -eq 0 ]] || { usage; exit 2; }
acquire_lock
cmd_pull
;;
destroy)
acquire_lock
cmd_destroy "$@"
;;
help|-h|--help)
usage
;;
*)
usage
exit 2
;;
esac
}
main "$@"
+78
View File
@@ -0,0 +1,78 @@
#!/bin/bash
set -euo pipefail
TEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DEPLOY_DIR="$(cd "${TEST_DIR}/.." && pwd)"
SCRIPT="${DEPLOY_DIR}/apple-container.sh"
TEST_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/sub2api-apple-test.XXXXXX")"
STATE_DIR="${TEST_ROOT}/state"
ENV_FILE="${TEST_ROOT}/sub2api.env"
cleanup() {
rm -rf "${TEST_ROOT}"
}
trap cleanup EXIT
fail() {
printf 'FAIL: %s\n' "$*" >&2
exit 1
}
assert_exists() {
[[ -e "$1" ]] || fail "Expected path to exist: $1"
}
assert_missing() {
[[ ! -e "$1" ]] || fail "Expected path to be absent: $1"
}
export FAKE_CONTAINER_STATE="${STATE_DIR}"
export PATH="${TEST_DIR}/fixtures/bin:${PATH}"
export SUB2API_ENV_FILE="${ENV_FILE}"
mkdir -p "${STATE_DIR}"
"${SCRIPT}" init
[[ "$(stat -f '%Lp' "${ENV_FILE}")" == "600" ]] || fail "init did not create a mode-600 env file"
grep -q '^POSTGRES_PASSWORD=change_this_secure_password$' "${ENV_FILE}" && fail "init retained the placeholder password"
chmod 644 "${ENV_FILE}"
if "${SCRIPT}" up >/dev/null 2>&1; then
fail "up accepted an insecure env file"
fi
chmod 600 "${ENV_FILE}"
"${SCRIPT}" up
assert_exists "${STATE_DIR}/containers/sub2api-apple"
assert_exists "${STATE_DIR}/containers/sub2api-apple-postgres"
assert_exists "${STATE_DIR}/containers/sub2api-apple-redis"
assert_exists "${STATE_DIR}/running/sub2api-apple"
"${SCRIPT}" status >/dev/null
"${SCRIPT}" up --recreate
assert_exists "${STATE_DIR}/running/sub2api-apple"
"${SCRIPT}" down
assert_missing "${STATE_DIR}/running/sub2api-apple"
assert_missing "${STATE_DIR}/running/sub2api-apple-postgres"
assert_missing "${STATE_DIR}/running/sub2api-apple-redis"
"${SCRIPT}" destroy --yes
assert_missing "${STATE_DIR}/containers/sub2api-apple"
assert_missing "${STATE_DIR}/networks/sub2api-apple"
assert_exists "${STATE_DIR}/volumes/sub2api-apple-data"
"${SCRIPT}" up
"${SCRIPT}" destroy --volumes --yes
assert_missing "${STATE_DIR}/volumes/sub2api-apple-data"
assert_missing "${STATE_DIR}/volumes/sub2api-apple-postgres-data"
assert_missing "${STATE_DIR}/volumes/sub2api-apple-redis-data"
touch "${STATE_DIR}/system-running"
touch "${STATE_DIR}/containers/sub2api-apple"
touch "${STATE_DIR}/unowned/container/sub2api-apple"
if "${SCRIPT}" status >/dev/null 2>&1; then
fail "status accepted an unowned same-name container"
fi
printf 'Apple container lifecycle tests passed.\n'
+164
View File
@@ -0,0 +1,164 @@
#!/bin/bash
set -eu
STATE_DIR="${FAKE_CONTAINER_STATE:?FAKE_CONTAINER_STATE is required}"
mkdir -p \
"${STATE_DIR}/containers" \
"${STATE_DIR}/running" \
"${STATE_DIR}/networks" \
"${STATE_DIR}/volumes" \
"${STATE_DIR}/unowned/container" \
"${STATE_DIR}/unowned/network" \
"${STATE_DIR}/unowned/volume"
list_names() {
local directory=$1
local path
for path in "${directory}"/*; do
[[ -e "${path}" ]] || continue
basename "${path}"
done
}
last_argument() {
local value=""
for value in "$@"; do :; done
printf '%s\n' "${value}"
}
inspect_resource() {
local resource_type=$1
local resource_name=$2
local label_value="apple-container"
local address="192.168.65.4/24"
if [[ -e "${STATE_DIR}/unowned/${resource_type}/${resource_name}" ]]; then
label_value="other"
fi
case "${resource_name}" in
sub2api-apple-postgres) address="192.168.65.2/24" ;;
sub2api-apple-redis) address="192.168.65.3/24" ;;
esac
printf '[{"configuration":{"labels":{"org.sub2api.stack":"%s"}},"status":{"networks":[{"ipv4Address":"%s"}]}}]\n' \
"${label_value}" "${address}"
}
command=${1-}
if [[ $# -gt 0 ]]; then shift; fi
case "${command}" in
--version)
echo "container CLI version 1.1.0 (build: release, commit: fake)"
;;
system)
subcommand=${1-}
case "${subcommand}" in
status) [[ -e "${STATE_DIR}/system-running" ]] ;;
start) touch "${STATE_DIR}/system-running" ;;
stop) rm -f "${STATE_DIR}/system-running" "${STATE_DIR}/running"/* ;;
*) exit 1 ;;
esac
;;
list)
include_all=false
for argument in "$@"; do
[[ "${argument}" == "--all" || "${argument}" == "-a" ]] && include_all=true
done
if [[ "${include_all}" == true ]]; then
list_names "${STATE_DIR}/containers"
else
list_names "${STATE_DIR}/running"
fi
;;
network)
subcommand=${1-}
shift || true
case "${subcommand}" in
list)
echo default
list_names "${STATE_DIR}/networks"
;;
create) touch "${STATE_DIR}/networks/$(last_argument "$@")" ;;
inspect) inspect_resource network "${1}" ;;
delete) rm -f "${STATE_DIR}/networks/${1}" ;;
*) exit 1 ;;
esac
;;
volume)
subcommand=${1-}
shift || true
case "${subcommand}" in
list) list_names "${STATE_DIR}/volumes" ;;
create) touch "${STATE_DIR}/volumes/$(last_argument "$@")" ;;
inspect) inspect_resource volume "${1}" ;;
delete) rm -f "${STATE_DIR}/volumes/${1}" ;;
*) exit 1 ;;
esac
;;
image)
subcommand=${1-}
case "${subcommand}" in
inspect|pull) exit 0 ;;
*) exit 1 ;;
esac
;;
create)
name=""
while [[ $# -gt 0 ]]; do
case "$1" in
--name)
name=$2
shift 2
;;
--label|--network|--platform|--ulimit|--env-file|--volume|--entrypoint|--publish)
shift 2
;;
*)
shift
;;
esac
done
[[ -n "${name}" ]]
touch "${STATE_DIR}/containers/${name}"
;;
inspect)
inspect_resource container "${1}"
;;
start)
touch "${STATE_DIR}/running/${1}"
;;
stop)
for argument in "$@"; do
case "${argument}" in
--time|--signal) skip_next=true ;;
[0-9]*|SIG*) ;;
*) rm -f "${STATE_DIR}/running/${argument}" ;;
esac
done
;;
delete)
for argument in "$@"; do
case "${argument}" in
--force|-f) ;;
*)
rm -f "${STATE_DIR}/running/${argument}"
rm -f "${STATE_DIR}/containers/${argument}"
;;
esac
done
;;
exec)
echo 1
;;
logs|copy)
exit 0
;;
*)
echo "Unsupported fake container command: ${command} $*" >&2
exit 1
;;
esac
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
set -eu
printf '{"status":"ok"}\n'