Merge branch 'cd-pipeline' into dev
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
name: Deploy DeepDrftAPI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, dev]
|
||||
paths:
|
||||
- 'DeepDrftAPI/**'
|
||||
- 'DeepDrftData/**'
|
||||
- 'DeepDrftContent/**'
|
||||
- 'DeepDrftModels/**'
|
||||
- '.gitea/workflows/deploy-api.yml'
|
||||
- 'deploy/systemd/deepdrftapi.service'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build, Publish & Bundle
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '10.0.x'
|
||||
|
||||
- name: Install dotnet-ef tool
|
||||
run: |
|
||||
dotnet tool install --global dotnet-ef
|
||||
echo "$HOME/.dotnet/tools" >> $GITHUB_PATH
|
||||
|
||||
- name: Restore
|
||||
run: dotnet restore DeepDrftAPI/DeepDrftAPI.csproj -r linux-x64
|
||||
|
||||
# Build with RID so --no-build is valid for the Publish step below.
|
||||
- name: Build
|
||||
run: dotnet build DeepDrftAPI/DeepDrftAPI.csproj -c Release -r linux-x64 --self-contained --no-restore
|
||||
|
||||
# Add: dotnet test DeepDrftTests/DeepDrftTests.csproj when API integration tests exist.
|
||||
|
||||
- name: Publish (self-contained linux-x64)
|
||||
run: |
|
||||
dotnet publish DeepDrftAPI/DeepDrftAPI.csproj \
|
||||
-c Release \
|
||||
-r linux-x64 \
|
||||
--self-contained \
|
||||
--no-build \
|
||||
-o DeepDrftAPI/publish
|
||||
|
||||
# DeepDrftContextFactory reads environment/connections.json at design time.
|
||||
# Write a parseable dummy so the factory does not throw during bundle construction.
|
||||
# The bundle only needs the provider type, not a live database connection.
|
||||
- name: Write dummy connections file for EF bundle
|
||||
run: |
|
||||
mkdir -p DeepDrftAPI/environment
|
||||
echo '{"ConnectionStrings":{"DefaultConnection":"Host=localhost;Database=dummy;Username=dummy","Auth":"Host=localhost;Database=dummy;Username=dummy"}}' > DeepDrftAPI/environment/connections.json
|
||||
|
||||
# EF bundle: self-contained binary that applies DeepDrftContext migrations on the host
|
||||
# without the .NET SDK. AuthBlocks' Identity DB is NOT covered here — it self-migrates
|
||||
# via UseAuthBlocksStartupAsync() on first boot.
|
||||
- name: Bundle EF migrations
|
||||
run: |
|
||||
dotnet ef migrations bundle \
|
||||
--project DeepDrftData/DeepDrftData.csproj \
|
||||
--startup-project DeepDrftAPI/DeepDrftAPI.csproj \
|
||||
--context DeepDrftContext \
|
||||
--configuration Release \
|
||||
--output deepdrft-migrations-bundle \
|
||||
--self-contained \
|
||||
-r linux-x64
|
||||
|
||||
- name: Package
|
||||
run: tar -czf deepdrft-api.tar.gz -C DeepDrftAPI/publish .
|
||||
|
||||
- name: Upload artifacts (archive + bundle)
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: deepdrft-api
|
||||
path: |
|
||||
deepdrft-api.tar.gz
|
||||
deepdrft-migrations-bundle
|
||||
retention-days: 1
|
||||
|
||||
deploy:
|
||||
name: Deploy
|
||||
needs: build
|
||||
runs-on: ubuntu-24.04
|
||||
if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/dev')
|
||||
env:
|
||||
DEPLOY_HOST: ${{ github.ref == 'refs/heads/master' && 'prod.cerebellumsoftworks.com' || 'dch7.cerebellumsoftworks.com' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: deepdrft-api
|
||||
path: staging/
|
||||
|
||||
- name: Install SSH tooling
|
||||
run: apt-get update && apt-get install -y --no-install-recommends openssh-client rsync
|
||||
|
||||
- name: Configure SSH
|
||||
env:
|
||||
PROD_KEY: ${{ secrets.DEEPDRFT_PROD_SSH_DEPLOY }}
|
||||
BETA_KEY: ${{ secrets.DEEPDRFT_DCH7_SSH_DEPLOY }}
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
if [ "$DEPLOY_HOST" = "prod.cerebellumsoftworks.com" ]; then
|
||||
echo "$PROD_KEY" > ~/.ssh/deepdrft_ed25519
|
||||
else
|
||||
echo "$BETA_KEY" > ~/.ssh/deepdrft_ed25519
|
||||
fi
|
||||
chmod 600 ~/.ssh/deepdrft_ed25519
|
||||
ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts || { echo "ssh-keyscan failed for $DEPLOY_HOST"; exit 1; }
|
||||
|
||||
- name: Rsync archive, bundle, and unit file to staging
|
||||
run: |
|
||||
chmod +x staging/deepdrft-migrations-bundle
|
||||
rsync -e "ssh -i ~/.ssh/deepdrft_ed25519 -o StrictHostKeyChecking=yes" \
|
||||
staging/deepdrft-api.tar.gz \
|
||||
staging/deepdrft-migrations-bundle \
|
||||
deploy/systemd/deepdrftapi.service \
|
||||
deepdrft@$DEPLOY_HOST:
|
||||
|
||||
- name: Trigger deploy on host
|
||||
run: |
|
||||
ssh -i ~/.ssh/deepdrft_ed25519 -o StrictHostKeyChecking=yes \
|
||||
deepdrft@$DEPLOY_HOST deploy-api
|
||||
@@ -0,0 +1,82 @@
|
||||
name: Deploy DeepDrftManager
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, dev]
|
||||
paths:
|
||||
- 'DeepDrftManager/**'
|
||||
- 'DeepDrftShared.Client/**'
|
||||
- 'DeepDrftModels/**'
|
||||
- '.gitea/workflows/deploy-manager.yml'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build & Publish
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '10.0.x'
|
||||
|
||||
# No test project scoped to DeepDrftManager — add one and wire it here when ready.
|
||||
|
||||
- name: Publish (self-contained linux-x64)
|
||||
run: |
|
||||
dotnet publish DeepDrftManager/DeepDrftManager.csproj \
|
||||
-c Release \
|
||||
-r linux-x64 \
|
||||
--self-contained \
|
||||
-o DeepDrftManager/publish
|
||||
|
||||
- name: Package
|
||||
run: tar -czf deepdrft-manager.tar.gz -C DeepDrftManager/publish .
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: deepdrft-manager
|
||||
path: deepdrft-manager.tar.gz
|
||||
retention-days: 1
|
||||
|
||||
deploy:
|
||||
name: Deploy
|
||||
needs: build
|
||||
runs-on: ubuntu-24.04
|
||||
if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/dev')
|
||||
env:
|
||||
DEPLOY_HOST: ${{ github.ref == 'refs/heads/master' && 'prod.cerebellumsoftworks.com' || 'dch7.cerebellumsoftworks.com' }}
|
||||
steps:
|
||||
- name: Download artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: deepdrft-manager
|
||||
|
||||
- name: Install SSH tooling
|
||||
run: apt-get update && apt-get install -y --no-install-recommends openssh-client rsync
|
||||
|
||||
- name: Configure SSH
|
||||
env:
|
||||
PROD_KEY: ${{ secrets.DEEPDRFT_PROD_SSH_DEPLOY }}
|
||||
BETA_KEY: ${{ secrets.DEEPDRFT_DCH7_SSH_DEPLOY }}
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
if [ "$DEPLOY_HOST" = "prod.cerebellumsoftworks.com" ]; then
|
||||
echo "$PROD_KEY" > ~/.ssh/deepdrft_ed25519
|
||||
else
|
||||
echo "$BETA_KEY" > ~/.ssh/deepdrft_ed25519
|
||||
fi
|
||||
chmod 600 ~/.ssh/deepdrft_ed25519
|
||||
ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts || { echo "ssh-keyscan failed for $DEPLOY_HOST"; exit 1; }
|
||||
|
||||
- name: Rsync archive to staging
|
||||
run: |
|
||||
rsync -e "ssh -i ~/.ssh/deepdrft_ed25519 -o StrictHostKeyChecking=yes" \
|
||||
deepdrft-manager.tar.gz \
|
||||
deepdrft@$DEPLOY_HOST:
|
||||
|
||||
- name: Trigger deploy on host
|
||||
run: |
|
||||
ssh -i ~/.ssh/deepdrft_ed25519 -o StrictHostKeyChecking=yes \
|
||||
deepdrft@$DEPLOY_HOST deploy-manager
|
||||
@@ -0,0 +1,86 @@
|
||||
name: Deploy DeepDrftPublic
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, dev]
|
||||
paths:
|
||||
- 'DeepDrftPublic/**'
|
||||
- 'DeepDrftPublic.Client/**'
|
||||
- 'DeepDrftShared.Client/**'
|
||||
- 'DeepDrftModels/**'
|
||||
- '.gitea/workflows/deploy-public.yml'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build & Publish
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '10.0.x'
|
||||
|
||||
- name: Install wasm-tools workload
|
||||
run: dotnet workload install wasm-tools
|
||||
|
||||
# No test project scoped to DeepDrftPublic — add one and wire it here when ready.
|
||||
|
||||
- name: Publish (self-contained linux-x64)
|
||||
run: |
|
||||
dotnet publish DeepDrftPublic/DeepDrftPublic.csproj \
|
||||
-c Release \
|
||||
-r linux-x64 \
|
||||
--self-contained \
|
||||
-o DeepDrftPublic/publish
|
||||
|
||||
- name: Package
|
||||
run: tar -czf deepdrft-public.tar.gz -C DeepDrftPublic/publish .
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: deepdrft-public
|
||||
path: deepdrft-public.tar.gz
|
||||
retention-days: 1
|
||||
|
||||
deploy:
|
||||
name: Deploy
|
||||
needs: build
|
||||
runs-on: ubuntu-24.04
|
||||
if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/dev')
|
||||
env:
|
||||
DEPLOY_HOST: ${{ github.ref == 'refs/heads/master' && 'prod.cerebellumsoftworks.com' || 'dch7.cerebellumsoftworks.com' }}
|
||||
steps:
|
||||
- name: Download artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: deepdrft-public
|
||||
|
||||
- name: Install SSH tooling
|
||||
run: apt-get update && apt-get install -y --no-install-recommends openssh-client rsync
|
||||
|
||||
- name: Configure SSH
|
||||
env:
|
||||
PROD_KEY: ${{ secrets.DEEPDRFT_PROD_SSH_DEPLOY }}
|
||||
BETA_KEY: ${{ secrets.DEEPDRFT_DCH7_SSH_DEPLOY }}
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
if [ "$DEPLOY_HOST" = "prod.cerebellumsoftworks.com" ]; then
|
||||
echo "$PROD_KEY" > ~/.ssh/deepdrft_ed25519
|
||||
else
|
||||
echo "$BETA_KEY" > ~/.ssh/deepdrft_ed25519
|
||||
fi
|
||||
chmod 600 ~/.ssh/deepdrft_ed25519
|
||||
ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts || { echo "ssh-keyscan failed for $DEPLOY_HOST"; exit 1; }
|
||||
|
||||
- name: Rsync archive to staging
|
||||
run: |
|
||||
rsync -e "ssh -i ~/.ssh/deepdrft_ed25519 -o StrictHostKeyChecking=yes" \
|
||||
deepdrft-public.tar.gz \
|
||||
deepdrft@$DEPLOY_HOST:
|
||||
|
||||
- name: Trigger deploy on host
|
||||
run: |
|
||||
ssh -i ~/.ssh/deepdrft_ed25519 -o StrictHostKeyChecking=yes \
|
||||
deepdrft@$DEPLOY_HOST deploy-public
|
||||
@@ -0,0 +1,65 @@
|
||||
name: Package install tarball
|
||||
|
||||
# Authentication: uses the built-in gitea.token (auto-injected per run).
|
||||
# No manual secret required.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, dev]
|
||||
paths:
|
||||
- 'deploy/**'
|
||||
|
||||
jobs:
|
||||
package:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build install tarball
|
||||
run: |
|
||||
tar -czf deepdrft-install.tar.gz \
|
||||
--exclude=deploy/bootstrap.sh \
|
||||
--transform 's|^deploy/||' \
|
||||
deploy/
|
||||
|
||||
- name: Create Gitea release
|
||||
env:
|
||||
GITEA_TOKEN: ${{ gitea.token }}
|
||||
GITEA_SERVER_URL: ${{ gitea.server_url }}
|
||||
GITEA_REPO: ${{ gitea.repository }}
|
||||
run: |
|
||||
SHORT_SHA="${GITHUB_SHA::7}"
|
||||
if [ "$GITHUB_REF" = "refs/heads/master" ]; then
|
||||
TAG="install-$(date +%Y%m%d)-${SHORT_SHA}"
|
||||
PRERELEASE="false"
|
||||
else
|
||||
TAG="beta-$(date +%Y%m%d)-${SHORT_SHA}"
|
||||
PRERELEASE="true"
|
||||
fi
|
||||
|
||||
# Create the release via Gitea API
|
||||
RELEASE_URL=$(curl -s -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${GITEA_SERVER_URL}/api/v1/repos/${GITEA_REPO}/releases" \
|
||||
-d "{
|
||||
\"tag_name\": \"${TAG}\",
|
||||
\"name\": \"Install package ${TAG}\",
|
||||
\"body\": \"Self-contained install tarball. Commit: ${GITHUB_SHA}\",
|
||||
\"draft\": false,
|
||||
\"prerelease\": ${PRERELEASE}
|
||||
}" | jq -r '.upload_url')
|
||||
|
||||
# upload_url from Gitea includes {?name,label} template — strip it
|
||||
UPLOAD_BASE="${RELEASE_URL%\{*\}}"
|
||||
|
||||
curl -s -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
"${UPLOAD_BASE}?name=deepdrft-install.tar.gz" \
|
||||
--data-binary @deepdrft-install.tar.gz
|
||||
|
||||
echo "Released as tag: ${TAG}"
|
||||
echo "Asset URL: ${GITEA_SERVER_URL}/${GITEA_REPO}/releases/download/${TAG}/deepdrft-install.tar.gz"
|
||||
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env bash
|
||||
# deploy/bootstrap.sh
|
||||
#
|
||||
# Self-contained first-time host bootstrap for the DeepDrft vertical.
|
||||
# Curl this onto a bare Ubuntu host and pipe to bash as root:
|
||||
#
|
||||
# For private repos (typical): download the release asset from the Gitea UI
|
||||
# on your local machine, scp it to the host, then run with INSTALL_PKG_PATH:
|
||||
#
|
||||
# scp deepdrft-install.tar.gz root@<host>:/tmp/
|
||||
# curl -fsSL .../deploy/bootstrap.sh | INSTALL_PKG_PATH=/tmp/deepdrft-install.tar.gz sudo bash
|
||||
#
|
||||
# For public repos: set INSTALL_PKG_URL to the release asset URL instead.
|
||||
#
|
||||
# curl -fsSL https://gitea.example.com/danielharvey/DeepDrftHome/raw/branch/master/deploy/bootstrap.sh \
|
||||
# | INSTALL_PKG_URL=https://gitea.example.com/danielharvey/DeepDrftHome/releases/download/<tag>/deepdrft-install.tar.gz \
|
||||
# sudo bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ── Output helpers ──────────────────────────────────────────────────────────────
|
||||
step() { echo; echo "=== $* ==="; }
|
||||
die() { echo; echo "[ERROR] $*" >&2; exit 1; }
|
||||
|
||||
# ── Preflight ──────────────────────────────────────────────────────────────────
|
||||
step "DeepDrft bootstrap"
|
||||
|
||||
if [[ "${EUID}" -ne 0 ]]; then
|
||||
die "Must run as root: sudo bash bootstrap.sh"
|
||||
fi
|
||||
|
||||
# ── OS prereqs ─────────────────────────────────────────────────────────────────
|
||||
step "Installing OS prereqs"
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq
|
||||
apt-get install -y --no-install-recommends \
|
||||
postgresql \
|
||||
nginx \
|
||||
rsync \
|
||||
openssl \
|
||||
jq \
|
||||
wget \
|
||||
ca-certificates
|
||||
|
||||
echo " [ok] apt packages installed"
|
||||
|
||||
# Ensure postgres is running — may need a moment after fresh install
|
||||
systemctl enable postgresql
|
||||
systemctl start postgresql
|
||||
sleep 2
|
||||
if ! pg_isready &>/dev/null; then
|
||||
die "PostgreSQL did not start. Check: journalctl -u postgresql"
|
||||
fi
|
||||
echo " [ok] PostgreSQL running"
|
||||
|
||||
# ── Install package resolution ─────────────────────────────────────────────────
|
||||
step "Install package"
|
||||
|
||||
TARBALL="/tmp/deepdrft-install.tar.gz"
|
||||
EXTRACT_DIR="/tmp/deepdrft-install"
|
||||
|
||||
if [[ -n "${INSTALL_PKG_PATH:-}" ]]; then
|
||||
# Case 1: caller supplied an explicit local path
|
||||
if [[ ! -f "${INSTALL_PKG_PATH}" ]]; then
|
||||
die "INSTALL_PKG_PATH is set but file not found: ${INSTALL_PKG_PATH}"
|
||||
fi
|
||||
echo " Using local package: ${INSTALL_PKG_PATH}"
|
||||
TARBALL="${INSTALL_PKG_PATH}"
|
||||
elif [[ -n "${INSTALL_PKG_URL:-}" ]]; then
|
||||
# Case 2: URL supplied — download it
|
||||
echo " Downloading ${INSTALL_PKG_URL} ..."
|
||||
wget -q --show-progress -O "${TARBALL}" "${INSTALL_PKG_URL}"
|
||||
echo " [ok] Downloaded to ${TARBALL}"
|
||||
else
|
||||
# Nothing provided — prompt, explaining the private-repo workflow
|
||||
echo
|
||||
echo " The install package is a Gitea release asset (deepdrft-install.tar.gz)."
|
||||
echo " Because the repo is private, download it from the Gitea UI on your local"
|
||||
echo " machine and scp it to this host before continuing."
|
||||
echo
|
||||
echo " Option A — local path (recommended for private repos):"
|
||||
echo " scp deepdrft-install.tar.gz root@<this-host>:/tmp/"
|
||||
echo " Then re-run: INSTALL_PKG_PATH=/tmp/deepdrft-install.tar.gz bash bootstrap.sh"
|
||||
echo
|
||||
echo " Option B — URL (only works if the release is publicly accessible):"
|
||||
echo " Enter the URL below."
|
||||
echo
|
||||
read -rp " Local path to tarball (leave blank to enter a URL): " INSTALL_PKG_PATH_INPUT
|
||||
if [[ -n "${INSTALL_PKG_PATH_INPUT}" ]]; then
|
||||
if [[ ! -f "${INSTALL_PKG_PATH_INPUT}" ]]; then
|
||||
die "File not found: ${INSTALL_PKG_PATH_INPUT}"
|
||||
fi
|
||||
echo " Using local package: ${INSTALL_PKG_PATH_INPUT}"
|
||||
TARBALL="${INSTALL_PKG_PATH_INPUT}"
|
||||
else
|
||||
echo
|
||||
read -rp " INSTALL_PKG_URL: " INSTALL_PKG_URL
|
||||
if [[ -z "${INSTALL_PKG_URL}" ]]; then
|
||||
die "INSTALL_PKG_URL is required."
|
||||
fi
|
||||
echo " Downloading ${INSTALL_PKG_URL} ..."
|
||||
wget -q --show-progress -O "${TARBALL}" "${INSTALL_PKG_URL}"
|
||||
echo " [ok] Downloaded to ${TARBALL}"
|
||||
fi
|
||||
fi
|
||||
|
||||
rm -rf "${EXTRACT_DIR}"
|
||||
mkdir -p "${EXTRACT_DIR}"
|
||||
tar -xzf "${TARBALL}" -C "${EXTRACT_DIR}"
|
||||
echo " [ok] Extracted to ${EXTRACT_DIR}"
|
||||
|
||||
# ── Hand off to install.sh ─────────────────────────────────────────────────────
|
||||
step "Running install.sh"
|
||||
|
||||
if [[ ! -f "${EXTRACT_DIR}/install.sh" ]]; then
|
||||
die "install.sh not found in ${EXTRACT_DIR}. Tarball layout unexpected."
|
||||
fi
|
||||
|
||||
chmod +x "${EXTRACT_DIR}/install.sh"
|
||||
exec bash "${EXTRACT_DIR}/install.sh"
|
||||
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env bash
|
||||
# Installed to: /opt/<APP_USER>/bin/deploy-api.sh
|
||||
# Deployed by: deploy-api Gitea Actions workflow (ssh forced-command)
|
||||
#
|
||||
# Expects in ${APP_HOME}/staging/:
|
||||
# deepdrft-api.tar.gz -- published self-contained linux-x64 binary tree
|
||||
# deepdrft-migrations-bundle -- self-contained EF bundle (pre-built in CI)
|
||||
# deepdrftapi.service -- systemd unit file (optional)
|
||||
#
|
||||
# Migrations are applied BEFORE the service restarts via the EF bundle binary.
|
||||
# The bundle covers DeepDrftContext (track metadata DB) only.
|
||||
# AuthBlocks' Identity DB is NOT bundled here — UseAuthBlocksStartupAsync() applies
|
||||
# its own migrations and seeds the admin user on first service boot.
|
||||
#
|
||||
# The DB connection string is read from the host credential file at:
|
||||
# ${APP_HOME}/.config/credentials/connections.json
|
||||
# The DefaultConnection value is extracted and passed to the bundle.
|
||||
# No DB name is passed by CI — the host credential is the source of truth.
|
||||
#
|
||||
# The vault directory (${APP_HOME}/api/deepdrft/vaults) is NEVER touched on deploy.
|
||||
# It is persistent host state created by the installer. Do not add cleanup logic here.
|
||||
#
|
||||
# Paths are derived at runtime — no hardcoded usernames or home dirs.
|
||||
# APP_HOME comes from $HOME (sshd sets this for the app user).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
export XDG_RUNTIME_DIR="/run/user/$(id -u)"
|
||||
|
||||
APP_HOME="${HOME}"
|
||||
export PATH="${APP_HOME}/.local/bin:${PATH}"
|
||||
|
||||
STAGING="${APP_HOME}/staging"
|
||||
APPROOT="${APP_HOME}/api/deepdrft"
|
||||
ARCHIVE="deepdrft-api.tar.gz"
|
||||
BUNDLE="${STAGING}/deepdrft-migrations-bundle"
|
||||
CREDS_FILE="${APP_HOME}/.config/credentials/connections.json"
|
||||
|
||||
echo "[deploy-api] $(date -u +%Y-%m-%dT%H:%M:%SZ) starting"
|
||||
|
||||
# ── Read DB connection string from host credential ────────────────────────
|
||||
if [[ ! -f "${CREDS_FILE}" ]]; then
|
||||
echo "[deploy-api] ERROR: credentials file not found: ${CREDS_FILE}" >&2
|
||||
echo "[deploy-api] Run setup-step10-creds.sh to create it." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if command -v jq &>/dev/null; then
|
||||
DB_CONN="$(jq -r '.ConnectionStrings.DefaultConnection' "${CREDS_FILE}")"
|
||||
else
|
||||
# Fallback: extract with grep/sed if jq is not installed
|
||||
DB_CONN="$(grep -o '"DefaultConnection"[[:space:]]*:[[:space:]]*"[^"]*"' "${CREDS_FILE}" \
|
||||
| sed 's/.*"DefaultConnection"[[:space:]]*:[[:space:]]*"\([^"]*\)"/\1/')"
|
||||
fi
|
||||
|
||||
if [[ -z "${DB_CONN}" || "${DB_CONN}" == "null" ]]; then
|
||||
echo "[deploy-api] ERROR: could not parse DefaultConnection from ${CREDS_FILE}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[deploy-api] connection string resolved from ${CREDS_FILE}"
|
||||
|
||||
# ── Apply migrations (before touching the binary) ─────────────────────────
|
||||
chmod +x "${BUNDLE}"
|
||||
echo "[deploy-api] applying DeepDrftContext migrations via EF bundle"
|
||||
"${BUNDLE}" --connection "${DB_CONN}"
|
||||
rm -f "${BUNDLE}"
|
||||
echo "[deploy-api] migrations done"
|
||||
|
||||
# ── Swap in new binary tree ────────────────────────────────────────────────
|
||||
rm -rf "${APPROOT}/bin.prev"
|
||||
if [[ -d "${APPROOT}/bin" ]]; then
|
||||
mv "${APPROOT}/bin" "${APPROOT}/bin.prev"
|
||||
fi
|
||||
mkdir -p "${APPROOT}/bin"
|
||||
|
||||
tar -xzf "${STAGING}/${ARCHIVE}" -C "${APPROOT}/bin"
|
||||
rm -f "${STAGING}/${ARCHIVE}"
|
||||
|
||||
echo "[deploy-api] archive extracted"
|
||||
|
||||
# ── Apply environment files (host-managed, not in archive) ────────────────
|
||||
# ${APP_HOME}/api/deepdrft/environment/ may contain appsettings overrides.
|
||||
# These must NOT live inside the rsync-writable staging area so a bad deploy
|
||||
# cannot overwrite them.
|
||||
if [[ -d "${APPROOT}/environment" ]]; then
|
||||
shopt -s nullglob
|
||||
env_files=("${APPROOT}/environment/"*)
|
||||
shopt -u nullglob
|
||||
if [[ ${#env_files[@]} -gt 0 ]]; then
|
||||
mkdir -p "${APPROOT}/bin/environment"
|
||||
cp "${env_files[@]}" "${APPROOT}/bin/environment/"
|
||||
echo "[deploy-api] environment files applied"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Install systemd unit file (if present in staging) ─────────────────────
|
||||
if [[ -f "${STAGING}/deepdrftapi.service" ]]; then
|
||||
mkdir -p "${APP_HOME}/.config/systemd/user"
|
||||
cp "${STAGING}/deepdrftapi.service" "${APP_HOME}/.config/systemd/user/deepdrftapi.service"
|
||||
rm -f "${STAGING}/deepdrftapi.service"
|
||||
systemctl --user daemon-reload
|
||||
echo "[deploy-api] systemd unit file installed"
|
||||
fi
|
||||
|
||||
# ── Enable and restart service ─────────────────────────────────────────────
|
||||
systemctl --user enable deepdrftapi.service
|
||||
systemctl --user restart deepdrftapi.service
|
||||
systemctl --user is-active --quiet deepdrftapi.service \
|
||||
&& echo "[deploy-api] service is active" \
|
||||
|| { echo "[deploy-api] ERROR: service failed to start" >&2; exit 1; }
|
||||
|
||||
echo "[deploy-api] done"
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
# Installed to: /opt/<APP_USER>/bin/deploy-manager.sh
|
||||
# Deployed by: deploy-manager Gitea Actions workflow (ssh forced-command)
|
||||
#
|
||||
# Expects in ${APP_HOME}/staging/:
|
||||
# deepdrft-manager.tar.gz -- published self-contained linux-x64 binary tree
|
||||
#
|
||||
# DeepDrftManager receives its API URL + API key credential via systemd LoadCredential
|
||||
# (api-manager.json -> $CREDENTIALS_DIRECTORY/api at runtime). No env file copy needed.
|
||||
#
|
||||
# Paths are derived at runtime — no hardcoded usernames or home dirs.
|
||||
# APP_HOME comes from $HOME (sshd sets this for the app user).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
export XDG_RUNTIME_DIR="/run/user/$(id -u)"
|
||||
|
||||
APP_HOME="${HOME}"
|
||||
export PATH="${APP_HOME}/.local/bin:${PATH}"
|
||||
|
||||
STAGING="${APP_HOME}/staging"
|
||||
APPROOT="${APP_HOME}/manager"
|
||||
ARCHIVE="deepdrft-manager.tar.gz"
|
||||
|
||||
echo "[deploy-manager] $(date -u +%Y-%m-%dT%H:%M:%SZ) starting"
|
||||
|
||||
# ── Swap in new binary tree ────────────────────────────────────────────────
|
||||
rm -rf "${APPROOT}/bin.prev"
|
||||
if [[ -d "${APPROOT}/bin" ]]; then
|
||||
mv "${APPROOT}/bin" "${APPROOT}/bin.prev"
|
||||
fi
|
||||
mkdir -p "${APPROOT}/bin"
|
||||
|
||||
tar -xzf "${STAGING}/${ARCHIVE}" -C "${APPROOT}/bin"
|
||||
rm -f "${STAGING}/${ARCHIVE}"
|
||||
|
||||
echo "[deploy-manager] archive extracted"
|
||||
|
||||
# ── Enable and restart service ─────────────────────────────────────────────
|
||||
systemctl --user enable deepdrftmanager.service
|
||||
systemctl --user restart deepdrftmanager.service
|
||||
systemctl --user is-active --quiet deepdrftmanager.service \
|
||||
&& echo "[deploy-manager] service is active" \
|
||||
|| { echo "[deploy-manager] ERROR: service failed to start" >&2; exit 1; }
|
||||
|
||||
echo "[deploy-manager] done"
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
# Installed to: /opt/<APP_USER>/bin/deploy-public.sh
|
||||
# Deployed by: deploy-public Gitea Actions workflow (ssh forced-command)
|
||||
#
|
||||
# Expects in ${APP_HOME}/staging/:
|
||||
# deepdrft-public.tar.gz -- published self-contained linux-x64 binary tree
|
||||
#
|
||||
# DeepDrftPublic receives its API URL credential via systemd LoadCredential
|
||||
# (api-public.json -> $CREDENTIALS_DIRECTORY/api at runtime). No env file copy needed.
|
||||
#
|
||||
# Paths are derived at runtime — no hardcoded usernames or home dirs.
|
||||
# APP_HOME comes from $HOME (sshd sets this for the app user).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
export XDG_RUNTIME_DIR="/run/user/$(id -u)"
|
||||
|
||||
APP_HOME="${HOME}"
|
||||
export PATH="${APP_HOME}/.local/bin:${PATH}"
|
||||
|
||||
STAGING="${APP_HOME}/staging"
|
||||
APPROOT="${APP_HOME}/public"
|
||||
ARCHIVE="deepdrft-public.tar.gz"
|
||||
|
||||
echo "[deploy-public] $(date -u +%Y-%m-%dT%H:%M:%SZ) starting"
|
||||
|
||||
# ── Swap in new binary tree ────────────────────────────────────────────────
|
||||
rm -rf "${APPROOT}/bin.prev"
|
||||
if [[ -d "${APPROOT}/bin" ]]; then
|
||||
mv "${APPROOT}/bin" "${APPROOT}/bin.prev"
|
||||
fi
|
||||
mkdir -p "${APPROOT}/bin"
|
||||
|
||||
tar -xzf "${STAGING}/${ARCHIVE}" -C "${APPROOT}/bin"
|
||||
rm -f "${STAGING}/${ARCHIVE}"
|
||||
|
||||
echo "[deploy-public] archive extracted"
|
||||
|
||||
# ── Enable and restart service ─────────────────────────────────────────────
|
||||
systemctl --user enable deepdrftpublic.service
|
||||
systemctl --user restart deepdrftpublic.service
|
||||
systemctl --user is-active --quiet deepdrftpublic.service \
|
||||
&& echo "[deploy-public] service is active" \
|
||||
|| { echo "[deploy-public] ERROR: service failed to start" >&2; exit 1; }
|
||||
|
||||
echo "[deploy-public] done"
|
||||
@@ -0,0 +1,474 @@
|
||||
#!/usr/bin/env bash
|
||||
# deploy/install.sh
|
||||
#
|
||||
# One-shot installer for the DeepDrft vertical on Linux.
|
||||
# Runs from inside the extracted install tarball (or the deploy/ directory).
|
||||
# Invoked directly or via bootstrap.sh:
|
||||
#
|
||||
# sudo bash install.sh
|
||||
#
|
||||
# All file references use ${SCRIPT_DIR}/... — no repo checkout required on the host.
|
||||
#
|
||||
# HOME=${APP_HOME} is load-bearing: the systemd %h specifier must resolve to the app
|
||||
# home dir so that unit WorkingDirectory/ExecStart paths agree with the deploy scripts.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# ── Output helpers ─────────────────────────────────────────────────────────────
|
||||
step() { echo; echo "=== Step $1: $2 ==="; }
|
||||
info() { echo " [info] $*"; }
|
||||
ok() { echo " [ok] $*"; }
|
||||
die() { echo; echo " [ERROR] $*" >&2; exit 1; }
|
||||
|
||||
# ── Step 0: Preflight ──────────────────────────────────────────────────────────
|
||||
step 0 "Preflight"
|
||||
|
||||
if [[ "${EUID}" -ne 0 ]]; then
|
||||
die "Must run as root: sudo bash install.sh"
|
||||
fi
|
||||
|
||||
if ! command -v psql &>/dev/null; then
|
||||
die "psql not found on PATH. Install PostgreSQL before running this script."
|
||||
fi
|
||||
|
||||
if ! pg_isready &>/dev/null; then
|
||||
die "PostgreSQL is not accepting connections. Start it with: systemctl start postgresql"
|
||||
fi
|
||||
|
||||
if ! command -v nginx &>/dev/null; then
|
||||
die "nginx not found on PATH. Install nginx before running this script."
|
||||
fi
|
||||
|
||||
# rrsync ships in the rsync package; path varies by distro version
|
||||
RRSYNC_BIN=""
|
||||
if command -v rrsync &>/dev/null; then
|
||||
RRSYNC_BIN="$(command -v rrsync)"
|
||||
elif [[ -x "/usr/lib/rsync/rrsync" ]]; then
|
||||
RRSYNC_BIN="/usr/lib/rsync/rrsync"
|
||||
else
|
||||
die "rrsync not found. Install the rsync package: apt-get install -y rsync"
|
||||
fi
|
||||
|
||||
ok "psql present, PostgreSQL active, nginx present"
|
||||
ok "rrsync found at: ${RRSYNC_BIN}"
|
||||
|
||||
# ── Step 0b: Parameter collection ─────────────────────────────────────────────
|
||||
step "0b" "Configuration parameters"
|
||||
|
||||
echo
|
||||
echo " Press Enter to accept the default shown in [brackets]."
|
||||
echo
|
||||
|
||||
read -rp " App system username [deepdrft]: " APP_USER
|
||||
APP_USER="${APP_USER:-deepdrft}"
|
||||
|
||||
read -rp " App home directory [/${APP_USER}]: " APP_HOME
|
||||
APP_HOME="${APP_HOME:-/${APP_USER}}"
|
||||
|
||||
read -rp " PostgreSQL role name [${APP_USER}]: " PG_ROLE
|
||||
PG_ROLE="${PG_ROLE:-${APP_USER}}"
|
||||
|
||||
read -rp " Metadata database name [deepdrft-meta]: " DB_META
|
||||
DB_META="${DB_META:-deepdrft-meta}"
|
||||
|
||||
read -rp " Auth database name [deepdrft-auth]: " DB_AUTH
|
||||
DB_AUTH="${DB_AUTH:-deepdrft-auth}"
|
||||
|
||||
read -rp " Public domain [deepdrft.com]: " DOMAIN_PUBLIC
|
||||
DOMAIN_PUBLIC="${DOMAIN_PUBLIC:-deepdrft.com}"
|
||||
|
||||
read -rp " App subdomain [app.${DOMAIN_PUBLIC}]: " DOMAIN_APP
|
||||
DOMAIN_APP="${DOMAIN_APP:-app.${DOMAIN_PUBLIC}}"
|
||||
|
||||
CERTBOT_EMAIL=""
|
||||
while [[ -z "${CERTBOT_EMAIL}" ]]; do
|
||||
read -rp " Email for certbot TLS cert (required): " CERTBOT_EMAIL
|
||||
done
|
||||
|
||||
# Derived paths
|
||||
OPT_DIR="/opt/${APP_USER}/bin"
|
||||
AK_FILE="${APP_HOME}/.ssh/authorized_keys"
|
||||
|
||||
echo
|
||||
echo " ┌──────────────────────────────────────────────────────────────┐"
|
||||
echo " │ Installation settings │"
|
||||
echo " ├──────────────────────────────────────────────────────────────┤"
|
||||
printf " │ %-22s %-37s│\n" "APP_USER" "${APP_USER}"
|
||||
printf " │ %-22s %-37s│\n" "APP_HOME" "${APP_HOME}"
|
||||
printf " │ %-22s %-37s│\n" "PG_ROLE" "${PG_ROLE}"
|
||||
printf " │ %-22s %-37s│\n" "DB_META" "${DB_META}"
|
||||
printf " │ %-22s %-37s│\n" "DB_AUTH" "${DB_AUTH}"
|
||||
printf " │ %-22s %-37s│\n" "DOMAIN_PUBLIC" "${DOMAIN_PUBLIC}"
|
||||
printf " │ %-22s %-37s│\n" "DOMAIN_APP" "${DOMAIN_APP}"
|
||||
printf " │ %-22s %-37s│\n" "CERTBOT_EMAIL" "${CERTBOT_EMAIL}"
|
||||
printf " │ %-22s %-37s│\n" "OPT_DIR" "${OPT_DIR}"
|
||||
echo " └──────────────────────────────────────────────────────────────┘"
|
||||
echo
|
||||
|
||||
read -rp " Proceed with these settings? [y/N] " CONFIRM
|
||||
if [[ "${CONFIRM}" != "y" && "${CONFIRM}" != "Y" ]]; then
|
||||
die "Aborted by user."
|
||||
fi
|
||||
|
||||
# ── Step 1: Create app system user ─────────────────────────────────────────────
|
||||
step 1 "Create '${APP_USER}' system user"
|
||||
|
||||
if id "${APP_USER}" &>/dev/null; then
|
||||
ok "user '${APP_USER}' already exists — skipping"
|
||||
if [[ ! -d "${APP_HOME}" ]]; then
|
||||
info "Creating ${APP_HOME} (home dir missing)"
|
||||
mkdir -p "${APP_HOME}"
|
||||
fi
|
||||
chown "${APP_USER}:${APP_USER}" "${APP_HOME}"
|
||||
ok "${APP_HOME} owned by ${APP_USER}:${APP_USER}"
|
||||
else
|
||||
echo
|
||||
echo " About to create system user '${APP_USER}' with home directory ${APP_HOME}"
|
||||
echo " and shell /bin/bash (required for SSH forced-command dispatch)."
|
||||
echo
|
||||
read -rp " Proceed? [y/N] " CONFIRM_USER
|
||||
if [[ "${CONFIRM_USER}" != "y" && "${CONFIRM_USER}" != "Y" ]]; then
|
||||
die "Aborted by user."
|
||||
fi
|
||||
|
||||
useradd \
|
||||
--system \
|
||||
--home-dir "${APP_HOME}" \
|
||||
--create-home \
|
||||
--shell /bin/bash \
|
||||
"${APP_USER}"
|
||||
|
||||
ok "User created:"
|
||||
id "${APP_USER}"
|
||||
fi
|
||||
|
||||
# ── Step 2: Enable linger ──────────────────────────────────────────────────────
|
||||
step 2 "Enable linger for '${APP_USER}'"
|
||||
|
||||
if loginctl show-user "${APP_USER}" 2>/dev/null | grep -q "Linger=yes"; then
|
||||
ok "linger already enabled"
|
||||
else
|
||||
loginctl enable-linger "${APP_USER}"
|
||||
ok "linger enabled"
|
||||
fi
|
||||
|
||||
# ── Step 3: Directory layout ───────────────────────────────────────────────────
|
||||
step 3 "Directory layout"
|
||||
|
||||
# Run as app user so files are created with correct ownership
|
||||
sudo -u "${APP_USER}" mkdir -p "${APP_HOME}/public/bin"
|
||||
sudo -u "${APP_USER}" mkdir -p "${APP_HOME}/public/environment"
|
||||
sudo -u "${APP_USER}" mkdir -p "${APP_HOME}/manager/bin"
|
||||
sudo -u "${APP_USER}" mkdir -p "${APP_HOME}/manager/environment"
|
||||
sudo -u "${APP_USER}" mkdir -p "${APP_HOME}/api/deepdrft/bin"
|
||||
sudo -u "${APP_USER}" mkdir -p "${APP_HOME}/api/deepdrft/environment"
|
||||
# FileDatabase vault: persistent data, created once, never touched on deploy
|
||||
sudo -u "${APP_USER}" mkdir -p "${APP_HOME}/api/deepdrft/vaults"
|
||||
sudo -u "${APP_USER}" mkdir -p "${APP_HOME}/staging"
|
||||
sudo -u "${APP_USER}" mkdir -p "${APP_HOME}/.ssh"
|
||||
sudo -u "${APP_USER}" chmod 700 "${APP_HOME}/.ssh"
|
||||
|
||||
ok "Directory layout ready"
|
||||
|
||||
# ── Step 4: Deploy scripts ─────────────────────────────────────────────────────
|
||||
step 4 "Deploy scripts -> ${OPT_DIR}/"
|
||||
|
||||
mkdir -p "${OPT_DIR}"
|
||||
|
||||
# Always overwrite — idempotent for re-runs that pick up script changes.
|
||||
# ssh-wrapper is installed without .sh extension (authorized_keys points to it).
|
||||
install -m 750 -o "${APP_USER}" -g "${APP_USER}" \
|
||||
"${SCRIPT_DIR}/ssh-wrapper.sh" "${OPT_DIR}/ssh-wrapper"
|
||||
install -m 750 -o "${APP_USER}" -g "${APP_USER}" \
|
||||
"${SCRIPT_DIR}/deploy-public.sh" "${OPT_DIR}/deploy-public.sh"
|
||||
install -m 750 -o "${APP_USER}" -g "${APP_USER}" \
|
||||
"${SCRIPT_DIR}/deploy-manager.sh" "${OPT_DIR}/deploy-manager.sh"
|
||||
install -m 750 -o "${APP_USER}" -g "${APP_USER}" \
|
||||
"${SCRIPT_DIR}/deploy-api.sh" "${OPT_DIR}/deploy-api.sh"
|
||||
|
||||
ok "Scripts installed:"
|
||||
ls -la "${OPT_DIR}/"
|
||||
|
||||
# ── Step 5: Systemd user units ─────────────────────────────────────────────────
|
||||
step 5 "Systemd user units"
|
||||
|
||||
APP_UID="$(id -u "${APP_USER}")"
|
||||
APP_RUNTIME_DIR="/run/user/${APP_UID}"
|
||||
|
||||
mkdir -p "${APP_HOME}/.config/systemd/user"
|
||||
|
||||
# Always overwrite — idempotent for re-runs that pick up unit changes.
|
||||
cp "${SCRIPT_DIR}/systemd/deepdrftpublic.service" "${APP_HOME}/.config/systemd/user/"
|
||||
cp "${SCRIPT_DIR}/systemd/deepdrftmanager.service" "${APP_HOME}/.config/systemd/user/"
|
||||
cp "${SCRIPT_DIR}/systemd/deepdrftapi.service" "${APP_HOME}/.config/systemd/user/"
|
||||
|
||||
chown -R "${APP_USER}:${APP_USER}" "${APP_HOME}/.config/systemd"
|
||||
|
||||
# daemon-reload and enable. XDG_RUNTIME_DIR must be set explicitly — PAM may not
|
||||
# have set it for this non-interactive context, and it varies by distro.
|
||||
sudo -u "${APP_USER}" env "XDG_RUNTIME_DIR=${APP_RUNTIME_DIR}" systemctl --user daemon-reload
|
||||
|
||||
sudo -u "${APP_USER}" env "XDG_RUNTIME_DIR=${APP_RUNTIME_DIR}" systemctl --user enable \
|
||||
deepdrftpublic.service \
|
||||
deepdrftmanager.service \
|
||||
deepdrftapi.service
|
||||
|
||||
ok "Units installed and enabled (not started — binaries don't exist yet)"
|
||||
|
||||
# ── Step 6: Credentials ────────────────────────────────────────────────────────
|
||||
step 6 "Credentials"
|
||||
|
||||
install -d -m 700 -o "${APP_USER}" -g "${APP_USER}" "${APP_HOME}/.config/credentials"
|
||||
|
||||
CRED_COUNT="$(find "${APP_HOME}/.config/credentials/" -maxdepth 1 -name '*.json' 2>/dev/null | wc -l)"
|
||||
|
||||
if [[ "${CRED_COUNT}" -ge 6 ]]; then
|
||||
ok "All credential files present (${CRED_COUNT} found) — skipping"
|
||||
info "To refresh credentials, run: bash ${SCRIPT_DIR}/setup-step10-creds.sh"
|
||||
else
|
||||
info "${CRED_COUNT}/6 credential files present — running setup-step10-creds.sh"
|
||||
sudo -u "${APP_USER}" \
|
||||
env APP_USER="${APP_USER}" \
|
||||
APP_HOME="${APP_HOME}" \
|
||||
PG_ROLE="${PG_ROLE}" \
|
||||
DB_META="${DB_META}" \
|
||||
DB_AUTH="${DB_AUTH}" \
|
||||
DOMAIN_PUBLIC="${DOMAIN_PUBLIC}" \
|
||||
DOMAIN_APP="${DOMAIN_APP}" \
|
||||
bash "${SCRIPT_DIR}/setup-step10-creds.sh"
|
||||
fi
|
||||
|
||||
# ── Step 7: PostgreSQL role and databases ──────────────────────────────────────
|
||||
step 7 "PostgreSQL role and databases"
|
||||
|
||||
echo
|
||||
echo " The '${PG_ROLE}' PostgreSQL role needs a password for TCP connections."
|
||||
echo " Local peer-auth connections (used by the deploy scripts) do not use it,"
|
||||
echo " but the credential JSON files reference it for app TCP connections."
|
||||
echo
|
||||
|
||||
read -rsp " PostgreSQL password for the '${PG_ROLE}' role: " PG_PASSWORD
|
||||
echo
|
||||
read -rsp " Confirm password: " PG_PASSWORD_CONFIRM
|
||||
echo
|
||||
|
||||
if [[ "${PG_PASSWORD}" != "${PG_PASSWORD_CONFIRM}" ]]; then
|
||||
die "Passwords do not match."
|
||||
fi
|
||||
|
||||
if [[ -z "${PG_PASSWORD}" ]]; then
|
||||
die "Password cannot be empty."
|
||||
fi
|
||||
|
||||
unset PG_PASSWORD_CONFIRM
|
||||
|
||||
# Create role if it doesn't exist, then always set the password.
|
||||
# Password is embedded in the heredoc body (psql stdin) — not in argv,
|
||||
# so it does not appear in `ps aux`. PGPASSWORD env var is not used here
|
||||
# because postgres peer auth (the default for the postgres superuser) does
|
||||
# not require a connection password.
|
||||
info "Creating/updating '${PG_ROLE}' PostgreSQL role..."
|
||||
|
||||
sudo -u postgres psql <<SQL
|
||||
DO \$\$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${PG_ROLE}') THEN
|
||||
CREATE ROLE "${PG_ROLE}" WITH LOGIN PASSWORD '${PG_PASSWORD}';
|
||||
RAISE NOTICE 'Role ${PG_ROLE} created.';
|
||||
ELSE
|
||||
RAISE NOTICE 'Role ${PG_ROLE} already exists — updating password.';
|
||||
END IF;
|
||||
END
|
||||
\$\$;
|
||||
ALTER ROLE "${PG_ROLE}" WITH PASSWORD '${PG_PASSWORD}';
|
||||
SQL
|
||||
|
||||
unset PG_PASSWORD
|
||||
|
||||
info "Creating databases if not present..."
|
||||
|
||||
# Databases may have hyphens in their names — always double-quote in SQL.
|
||||
DB_META_EXISTS=$(sudo -u postgres psql -tc "SELECT 1 FROM pg_database WHERE datname = '${DB_META}'")
|
||||
if echo "$DB_META_EXISTS" | grep -q 1; then
|
||||
info "Database '${DB_META}' already exists — skipping"
|
||||
else
|
||||
sudo -u postgres psql -c "CREATE DATABASE \"${DB_META}\" OWNER \"${PG_ROLE}\";"
|
||||
ok "Database '${DB_META}' created"
|
||||
fi
|
||||
|
||||
DB_AUTH_EXISTS=$(sudo -u postgres psql -tc "SELECT 1 FROM pg_database WHERE datname = '${DB_AUTH}'")
|
||||
if echo "$DB_AUTH_EXISTS" | grep -q 1; then
|
||||
info "Database '${DB_AUTH}' already exists — skipping"
|
||||
else
|
||||
sudo -u postgres psql -c "CREATE DATABASE \"${DB_AUTH}\" OWNER \"${PG_ROLE}\";"
|
||||
ok "Database '${DB_AUTH}' created"
|
||||
fi
|
||||
|
||||
ok "Databases ready"
|
||||
|
||||
info "Verifying peer auth connections..."
|
||||
|
||||
if ! sudo -u "${APP_USER}" psql -U "${PG_ROLE}" -d "${DB_META}" -c '\conninfo'; then
|
||||
die "Peer auth failed for ${DB_META}. Check /etc/postgresql/*/main/pg_hba.conf has: local all all peer"
|
||||
fi
|
||||
ok "Peer auth verified for ${DB_META}"
|
||||
|
||||
if ! sudo -u "${APP_USER}" psql -U "${PG_ROLE}" -d "${DB_AUTH}" -c '\conninfo'; then
|
||||
die "Peer auth failed for ${DB_AUTH}. Check /etc/postgresql/*/main/pg_hba.conf has: local all all peer"
|
||||
fi
|
||||
ok "Peer auth verified for ${DB_AUTH}"
|
||||
|
||||
# ── Step 8: SSH authorized_keys ────────────────────────────────────────────────
|
||||
step 8 "SSH authorized_keys"
|
||||
|
||||
echo
|
||||
echo " Generate a CI deploy key on your LOCAL machine (not this host):"
|
||||
echo
|
||||
echo " ssh-keygen -t ed25519 -C \"gitea-ci-${APP_USER}-dch7\" -f ~/.ssh/gitea_${APP_USER}_dch7"
|
||||
echo
|
||||
echo " Then paste the PUBLIC key (.pub file contents) at the prompt below."
|
||||
echo " After setup completes, add the PRIVATE key to:"
|
||||
echo " Gitea -> repo -> Settings -> Secrets -> DEEPDRFT_DCH7_SSH_DEPLOY"
|
||||
echo " (or DEEPDRFT_PROD_SSH_DEPLOY for the production host)"
|
||||
echo
|
||||
|
||||
read -rp " Paste public key here: " PUBKEY
|
||||
|
||||
# Strip carriage returns and leading/trailing whitespace (Windows clipboard, paste artifacts)
|
||||
PUBKEY="$(printf '%s' "${PUBKEY}" | tr -d '\r' | xargs)"
|
||||
|
||||
if [[ -z "${PUBKEY}" ]]; then
|
||||
die "No public key provided."
|
||||
fi
|
||||
|
||||
# Validate it looks like an SSH public key
|
||||
if [[ "${PUBKEY}" != ssh-ed25519* ]]; then
|
||||
die "Key does not start with 'ssh-ed25519'. Only ed25519 keys are accepted for CI deploy."
|
||||
fi
|
||||
|
||||
if [[ -f "${AK_FILE}" ]] && grep -qF "${PUBKEY}" "${AK_FILE}"; then
|
||||
ok "Key already present in authorized_keys — skipping"
|
||||
else
|
||||
# Write the forced-command + restrict prefix before the key.
|
||||
# The 'restrict' keyword bundles no-port-forwarding, no-agent-forwarding,
|
||||
# no-X11-forwarding, no-pty, no-user-rc into one short token.
|
||||
AK_ENTRY="command=\"${OPT_DIR}/ssh-wrapper\",restrict ${PUBKEY}"
|
||||
echo "${AK_ENTRY}" >> "${AK_FILE}"
|
||||
ok "Key written to ${AK_FILE}"
|
||||
fi
|
||||
|
||||
chown "${APP_USER}:${APP_USER}" "${AK_FILE}"
|
||||
chmod 600 "${AK_FILE}"
|
||||
|
||||
unset PUBKEY
|
||||
|
||||
# ── Step 9: nginx ──────────────────────────────────────────────────────────────
|
||||
step 9 "nginx"
|
||||
|
||||
# Read templates, substitute domain placeholders, write to sites-available.
|
||||
# Templates use __DOMAIN_PUBLIC__ and __DOMAIN_APP__ so the files in the tarball
|
||||
# don't contain real hostnames — substitution happens at install time.
|
||||
sed -e "s|__DOMAIN_PUBLIC__|${DOMAIN_PUBLIC}|g" \
|
||||
"${SCRIPT_DIR}/nginx/deepdrft-public.conf" \
|
||||
> "/etc/nginx/sites-available/${DOMAIN_PUBLIC}.conf"
|
||||
|
||||
sed -e "s|__DOMAIN_APP__|${DOMAIN_APP}|g" \
|
||||
"${SCRIPT_DIR}/nginx/deepdrft-manager.conf" \
|
||||
> "/etc/nginx/sites-available/${DOMAIN_APP}.conf"
|
||||
|
||||
ln -sf "/etc/nginx/sites-available/${DOMAIN_PUBLIC}.conf" /etc/nginx/sites-enabled/
|
||||
ln -sf "/etc/nginx/sites-available/${DOMAIN_APP}.conf" /etc/nginx/sites-enabled/
|
||||
|
||||
rm -f /etc/nginx/sites-enabled/default
|
||||
|
||||
if ! nginx -t; then
|
||||
die "nginx config test failed — check ${SCRIPT_DIR}/nginx/*.conf"
|
||||
fi
|
||||
|
||||
systemctl reload nginx
|
||||
|
||||
ok "nginx configured and reloaded"
|
||||
|
||||
# ── Step 10: Summary ───────────────────────────────────────────────────────────
|
||||
step 10 "Summary"
|
||||
|
||||
cat <<SUMMARY
|
||||
|
||||
========================================================================
|
||||
DeepDrft vertical installation complete.
|
||||
========================================================================
|
||||
|
||||
Installed / configured:
|
||||
- System user '${APP_USER}' with HOME=${APP_HOME}
|
||||
- Linger enabled (user units survive without an active session)
|
||||
- Directory layout: ${APP_HOME}/{public,manager,api/deepdrft}/{bin,environment}
|
||||
- FileDatabase vault: ${APP_HOME}/api/deepdrft/vaults (persistent — never touched on deploy)
|
||||
- Deploy scripts in ${OPT_DIR}/ (mode 750, owner ${APP_USER})
|
||||
- Systemd user units installed and enabled (not started):
|
||||
deepdrftpublic.service
|
||||
deepdrftmanager.service
|
||||
deepdrftapi.service
|
||||
- Credentials in ${APP_HOME}/.config/credentials/ (6 x mode 600)
|
||||
- PostgreSQL role '${PG_ROLE}' and databases: ${DB_META}, ${DB_AUTH}
|
||||
- SSH authorized_keys with forced-command + restrict
|
||||
- nginx sites-available + sites-enabled, default site removed
|
||||
|
||||
------------------------------------------------------------------------
|
||||
Remaining manual steps:
|
||||
------------------------------------------------------------------------
|
||||
|
||||
1. Add the PRIVATE deploy key to Gitea:
|
||||
Gitea -> repo -> Settings -> Secrets -> DEEPDRFT_DCH7_SSH_DEPLOY
|
||||
(Value: full contents of ~/.ssh/gitea_${APP_USER}_dch7 on your local machine,
|
||||
including -----BEGIN and -----END lines)
|
||||
|
||||
2. Smoke-test the SSH forced-command chain from your LOCAL machine:
|
||||
|
||||
Layer 1 — connectivity and dispatch:
|
||||
ssh -i ~/.ssh/gitea_${APP_USER}_dch7 ${APP_USER}@<host> deploy-public
|
||||
# Expect: "[deploy-public] ... starting" then error about missing archive
|
||||
# (staging is empty at this stage). The exit non-zero is expected.
|
||||
|
||||
Layer 2 — catch-all blocks arbitrary commands:
|
||||
ssh -i ~/.ssh/gitea_${APP_USER}_dch7 ${APP_USER}@<host> id
|
||||
# Expect: "ssh-wrapper: unknown command: id" — you must NOT get a shell.
|
||||
|
||||
Layer 3 — rsync jail:
|
||||
echo test > /tmp/smoke-test.txt
|
||||
rsync -e "ssh -i ~/.ssh/gitea_${APP_USER}_dch7 -o StrictHostKeyChecking=yes" \\
|
||||
/tmp/smoke-test.txt ${APP_USER}@<host>:
|
||||
# Then verify on host: ls ${APP_HOME}/staging/smoke-test.txt
|
||||
|
||||
Layer 4 — all three trigger words:
|
||||
for cmd in deploy-public deploy-manager deploy-api; do
|
||||
echo "--- \$cmd ---"
|
||||
ssh -i ~/.ssh/gitea_${APP_USER}_dch7 ${APP_USER}@<host> "\$cmd" 2>&1 | head -5
|
||||
done
|
||||
|
||||
3. TLS via certbot (run AFTER DNS is pointing at this host):
|
||||
|
||||
snap install --classic certbot
|
||||
ln -sf /snap/bin/certbot /usr/bin/certbot
|
||||
|
||||
certbot --nginx \\
|
||||
--email ${CERTBOT_EMAIL} \\
|
||||
--agree-tos \\
|
||||
--no-eff-email \\
|
||||
-d ${DOMAIN_PUBLIC} \\
|
||||
-d ${DOMAIN_APP}
|
||||
|
||||
nginx -t && systemctl reload nginx
|
||||
certbot renew --dry-run
|
||||
systemctl status snap.certbot.renew.timer
|
||||
|
||||
Note: use the certbot SNAP (not apt) — the snap installs the correct
|
||||
snap.certbot.renew.timer systemd unit. The apt package leaves auto-renewal
|
||||
silently broken on Ubuntu.
|
||||
|
||||
4. First deploy — push to Gitea master to trigger CI.
|
||||
Services will start on first successful deploy.
|
||||
|
||||
------------------------------------------------------------------------
|
||||
SUMMARY
|
||||
@@ -0,0 +1,19 @@
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name __DOMAIN_APP__;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:5001;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# WebSocket support (Blazor InteractiveServer SignalR circuits)
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $http_connection;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name __DOMAIN_PUBLIC__;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:5000;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# WebSocket support (Blazor Server SignalR circuits + WASM interop)
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $http_connection;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
#!/usr/bin/env bash
|
||||
# deploy/setup-step10-creds.sh
|
||||
# Run as the app user on the host. Implements Step 6 of install.sh.
|
||||
# Prompts interactively for every secret, writes JSON credential files
|
||||
# directly to ${APP_HOME}/.config/credentials/.
|
||||
#
|
||||
# Called by install.sh (which exports APP_USER, APP_HOME, PG_ROLE, DB_META,
|
||||
# DB_AUTH, DOMAIN_PUBLIC, DOMAIN_APP before invoking). Can also be run
|
||||
# standalone — falls back to defaults so the script remains independently useful.
|
||||
#
|
||||
# Idempotency: safe to re-run. By default, credentials that already exist in
|
||||
# ${CREDDIR} are left alone (no re-prompting). Pass --force as the first arg to
|
||||
# re-prompt for every secret and overwrite all credential files.
|
||||
#
|
||||
# Credential files and their consumers:
|
||||
# filedatabase.json -> DeepDrftAPI (LoadCredential id: filedatabase)
|
||||
# apikey.json -> DeepDrftAPI (LoadCredential id: apikey)
|
||||
# connections.json -> DeepDrftAPI (LoadCredential id: connections)
|
||||
# authblocks.json -> DeepDrftAPI (LoadCredential id: authblocks)
|
||||
# api-public.json -> DeepDrftPublic (LoadCredential id: api)
|
||||
# api-manager.json -> DeepDrftManager (LoadCredential id: api)
|
||||
#
|
||||
# The LoadCredential ids (left of colon in the unit file) must exactly match
|
||||
# CredentialTools.ResolvePathOrThrow keys in the application code.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Escape a string for safe use as inline JSON value.
|
||||
# Escapes backslash then double-quote (the two characters that break JSON string literals).
|
||||
# Does not handle embedded newlines or control chars — avoid those in secrets.
|
||||
json_escape() {
|
||||
printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'
|
||||
}
|
||||
|
||||
# Accept vars from environment (exported by install.sh) or fall back to defaults.
|
||||
APP_USER="${APP_USER:-deepdrft}"
|
||||
export XDG_RUNTIME_DIR="/run/user/$(id -u "${APP_USER}" 2>/dev/null || id -u)"
|
||||
APP_HOME="${APP_HOME:-/${APP_USER}}"
|
||||
PG_ROLE="${PG_ROLE:-deepdrft}"
|
||||
DB_META="${DB_META:-deepdrft-meta}"
|
||||
DB_AUTH="${DB_AUTH:-deepdrft-auth}"
|
||||
DOMAIN_PUBLIC="${DOMAIN_PUBLIC:-deepdrft.com}"
|
||||
DOMAIN_APP="${DOMAIN_APP:-app.${DOMAIN_PUBLIC}}"
|
||||
|
||||
CREDDIR="${APP_HOME}/.config/credentials"
|
||||
EXPECTED_COUNT=6
|
||||
|
||||
# ── Parse args ────────────────────────────────────────────────────────────────
|
||||
FORCE=0
|
||||
if [[ "${1:-}" == "--force" ]]; then
|
||||
FORCE=1
|
||||
echo "[setup-step10-creds] --force: will re-prompt and overwrite all credentials"
|
||||
fi
|
||||
|
||||
# ── Credential directory ──────────────────────────────────────────────────────
|
||||
if [[ ! -d "${CREDDIR}" ]]; then
|
||||
mkdir -p "${CREDDIR}"
|
||||
chmod 700 "${CREDDIR}"
|
||||
fi
|
||||
echo "[setup-step10-creds] ${CREDDIR} ready"
|
||||
|
||||
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
# write_cred <name> <content>
|
||||
# Writes JSON content to ${CREDDIR}/<name>.json (mode 600, owner APP_USER).
|
||||
write_cred() {
|
||||
local name="$1"
|
||||
local content="$2"
|
||||
local tmp
|
||||
tmp="$(mktemp /tmp/deepdrft-cred-XXXXXXXX.json)"
|
||||
printf '%s\n' "${content}" > "${tmp}"
|
||||
install -m 600 -o "${APP_USER}" -g "${APP_USER}" "${tmp}" "${CREDDIR}/${name}.json"
|
||||
rm -f "${tmp}"
|
||||
echo "[setup-step10-creds] wrote ${name}.json"
|
||||
}
|
||||
|
||||
# need_cred <name>
|
||||
# Returns 0 (true) when the named credential should be (re)written:
|
||||
# - --force was passed, OR
|
||||
# - ${CREDDIR}/<name>.json does not yet exist.
|
||||
need_cred() {
|
||||
local name="$1"
|
||||
if [[ "${FORCE}" -eq 1 ]]; then
|
||||
return 0
|
||||
fi
|
||||
if [[ ! -f "${CREDDIR}/${name}.json" ]]; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# ── 1. filedatabase.json — no prompts, path fully templated ──────────────────
|
||||
if need_cred "filedatabase"; then
|
||||
write_cred "filedatabase" \
|
||||
"{\"FileDatabaseSettings\":{\"VaultPath\":\"${APP_HOME}/api/deepdrft/vaults\"}}"
|
||||
else
|
||||
echo "[setup-step10-creds] filedatabase.json already exists, skipping"
|
||||
fi
|
||||
|
||||
# ── 2. apikey.json — prompt or generate; value reused in api-manager.json ────
|
||||
API_KEY=""
|
||||
if need_cred "apikey"; then
|
||||
echo
|
||||
read -rp " API key (leave blank to generate with openssl rand -hex 32): " API_KEY_INPUT
|
||||
if [[ -z "${API_KEY_INPUT}" ]]; then
|
||||
API_KEY="$(openssl rand -hex 32)"
|
||||
echo " [generated] API key: ${API_KEY}"
|
||||
else
|
||||
API_KEY="${API_KEY_INPUT}"
|
||||
fi
|
||||
unset API_KEY_INPUT
|
||||
write_cred "apikey" \
|
||||
"{\"ApiKeySettings\":{\"ApiKey\":\"$(json_escape "${API_KEY}")\"}}"
|
||||
else
|
||||
echo "[setup-step10-creds] apikey.json already exists, skipping"
|
||||
# Still need the value for api-manager.json if that's also being written.
|
||||
# Read it back from the existing file if jq is available, otherwise prompt again.
|
||||
if need_cred "api-manager"; then
|
||||
if command -v jq &>/dev/null; then
|
||||
API_KEY="$(jq -r '.ApiKeySettings.ApiKey' "${CREDDIR}/apikey.json")"
|
||||
echo "[setup-step10-creds] API key read from existing apikey.json"
|
||||
else
|
||||
echo
|
||||
echo " apikey.json exists but api-manager.json is missing."
|
||||
read -rp " Re-enter the API key (needed for api-manager.json): " API_KEY
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── 3. connections.json — prompt for PG password; DB names from env vars ─────
|
||||
if need_cred "connections"; then
|
||||
echo
|
||||
echo " PostgreSQL connection strings for DeepDrftAPI."
|
||||
echo " DB names: meta='${DB_META}', auth='${DB_AUTH}', role='${PG_ROLE}'"
|
||||
echo
|
||||
read -rsp " PostgreSQL password for the '${PG_ROLE}' role: " PG_PASSWORD
|
||||
echo
|
||||
read -rsp " Confirm password: " PG_PASSWORD_CONFIRM
|
||||
echo
|
||||
if [[ "${PG_PASSWORD}" != "${PG_PASSWORD_CONFIRM}" ]]; then
|
||||
echo "[setup-step10-creds] ERROR: passwords do not match" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ -z "${PG_PASSWORD}" ]]; then
|
||||
echo "[setup-step10-creds] ERROR: password cannot be empty" >&2
|
||||
exit 1
|
||||
fi
|
||||
unset PG_PASSWORD_CONFIRM
|
||||
|
||||
META_CONN="Host=localhost;Database=${DB_META};Username=${PG_ROLE};Password=$(json_escape "${PG_PASSWORD}")"
|
||||
AUTH_CONN="Host=localhost;Database=${DB_AUTH};Username=${PG_ROLE};Password=$(json_escape "${PG_PASSWORD}")"
|
||||
write_cred "connections" \
|
||||
"{\"ConnectionStrings\":{\"DefaultConnection\":\"${META_CONN}\",\"Auth\":\"${AUTH_CONN}\"}}"
|
||||
unset PG_PASSWORD META_CONN AUTH_CONN
|
||||
else
|
||||
echo "[setup-step10-creds] connections.json already exists, skipping"
|
||||
fi
|
||||
|
||||
# ── 4. authblocks.json — prompt for all AuthBlocks secrets ───────────────────
|
||||
if need_cred "authblocks"; then
|
||||
echo
|
||||
echo " AuthBlocks configuration (JWT, email, admin account)."
|
||||
echo
|
||||
|
||||
# JWT secret
|
||||
read -rp " JWT secret (leave blank to generate): " JWT_SECRET_INPUT
|
||||
if [[ -z "${JWT_SECRET_INPUT}" ]]; then
|
||||
JWT_SECRET="$(openssl rand -hex 32)"
|
||||
echo " [generated] JWT secret"
|
||||
else
|
||||
JWT_SECRET="${JWT_SECRET_INPUT}"
|
||||
fi
|
||||
unset JWT_SECRET_INPUT
|
||||
|
||||
# JWT issuer / audience
|
||||
read -rp " JWT issuer (e.g. https://${DOMAIN_PUBLIC}): " JWT_ISSUER
|
||||
read -rp " JWT audience (e.g. https://${DOMAIN_PUBLIC}): " JWT_AUDIENCE
|
||||
|
||||
# Email
|
||||
echo
|
||||
echo " Email provider (SMTP/API — used by AuthBlocks for verification emails)."
|
||||
read -rp " Email host (SMTP server or API host): " EMAIL_HOST
|
||||
read -rsp " Email token (API key / SMTP password): " EMAIL_TOKEN
|
||||
echo
|
||||
|
||||
# Admin account
|
||||
echo
|
||||
echo " Initial admin account for DeepDrft."
|
||||
read -rp " Admin username: " ADMIN_USERNAME
|
||||
read -rp " Admin email: " ADMIN_EMAIL
|
||||
read -rsp " Admin password: " ADMIN_PASSWORD
|
||||
echo
|
||||
read -rsp " Confirm admin password: " ADMIN_PASSWORD_CONFIRM
|
||||
echo
|
||||
if [[ "${ADMIN_PASSWORD}" != "${ADMIN_PASSWORD_CONFIRM}" ]]; then
|
||||
echo "[setup-step10-creds] ERROR: admin passwords do not match" >&2
|
||||
exit 1
|
||||
fi
|
||||
unset ADMIN_PASSWORD_CONFIRM
|
||||
|
||||
# Support email
|
||||
read -rp " Support email address: " SUPPORT_EMAIL
|
||||
|
||||
write_cred "authblocks" "$(cat <<JSON
|
||||
{"AuthBlocks":{"Jwt":{"Secret":"$(json_escape "${JWT_SECRET}")","Issuer":"$(json_escape "${JWT_ISSUER}")","Audience":"$(json_escape "${JWT_AUDIENCE}")"},"Email":{"Host":"$(json_escape "${EMAIL_HOST}")","Token":"$(json_escape "${EMAIL_TOKEN}")"},"Admin":{"UserName":"$(json_escape "${ADMIN_USERNAME}")","Email":"$(json_escape "${ADMIN_EMAIL}")","Password":"$(json_escape "${ADMIN_PASSWORD}")"},"SupportEmail":"$(json_escape "${SUPPORT_EMAIL}")"}}
|
||||
JSON
|
||||
)"
|
||||
unset JWT_SECRET JWT_ISSUER JWT_AUDIENCE EMAIL_HOST EMAIL_TOKEN
|
||||
unset ADMIN_USERNAME ADMIN_EMAIL ADMIN_PASSWORD SUPPORT_EMAIL
|
||||
else
|
||||
echo "[setup-step10-creds] authblocks.json already exists, skipping"
|
||||
fi
|
||||
|
||||
# ── 5. api-public.json — no prompts, static localhost URL ────────────────────
|
||||
if need_cred "api-public"; then
|
||||
write_cred "api-public" \
|
||||
'{"Api":{"ContentApiUrl":"http://localhost:5002"}}'
|
||||
else
|
||||
echo "[setup-step10-creds] api-public.json already exists, skipping"
|
||||
fi
|
||||
|
||||
# ── 6. api-manager.json — reuses API key from step 2 ─────────────────────────
|
||||
if need_cred "api-manager"; then
|
||||
if [[ -z "${API_KEY}" ]]; then
|
||||
echo
|
||||
echo " api-manager.json needs the same API key as apikey.json."
|
||||
read -rp " Enter the API key: " API_KEY
|
||||
fi
|
||||
write_cred "api-manager" \
|
||||
"{\"Api\":{\"ContentApiUrl\":\"http://localhost:5002\",\"ContentApiKey\":\"$(json_escape "${API_KEY}")\"}}"
|
||||
unset API_KEY
|
||||
else
|
||||
echo "[setup-step10-creds] api-manager.json already exists, skipping"
|
||||
unset API_KEY 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# ── Verify ────────────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "[setup-step10-creds] verifying ${CREDDIR}:"
|
||||
ls -la "${CREDDIR}/"
|
||||
|
||||
ACTUAL_COUNT="$(ls "${CREDDIR}/"*.json 2>/dev/null | wc -l)"
|
||||
if [[ "${ACTUAL_COUNT}" -ne "${EXPECTED_COUNT}" ]]; then
|
||||
echo "[setup-step10-creds] ERROR: expected ${EXPECTED_COUNT} .json files, found ${ACTUAL_COUNT}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[setup-step10-creds] all ${EXPECTED_COUNT} credentials present — step 6 complete"
|
||||
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
# Installed to: /opt/<APP_USER>/bin/ssh-wrapper
|
||||
#
|
||||
# Forced-command wrapper for the CI deploy key.
|
||||
# Install in ~<APP_USER>/.ssh/authorized_keys as:
|
||||
#
|
||||
# command="/opt/<APP_USER>/bin/ssh-wrapper",restrict ssh-ed25519 AAAA... gitea-ci-deploy
|
||||
#
|
||||
# The 'restrict' keyword covers no-port-forwarding, no-agent-forwarding,
|
||||
# no-X11-forwarding, no-pty, no-user-rc in one token.
|
||||
#
|
||||
# Supported commands dispatched by SSH_ORIGINAL_COMMAND:
|
||||
# rsync --server ... -> rrsync jail (staging uploads)
|
||||
# deploy-public -> <OPT_DIR>/deploy-public.sh
|
||||
# deploy-manager -> <OPT_DIR>/deploy-manager.sh
|
||||
# deploy-api -> <OPT_DIR>/deploy-api.sh (no trailing arg — reads creds from host)
|
||||
#
|
||||
# Paths are derived at runtime — no hardcoded usernames or home dirs.
|
||||
# APP_HOME comes from $HOME (sshd sets this for the app user).
|
||||
# OPT_DIR is the directory containing this script.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Derive paths from runtime context — no hardcoded APP_USER or APP_HOME.
|
||||
# sshd sets $HOME to the app user's home directory for forced-command sessions.
|
||||
APP_HOME="${HOME}"
|
||||
OPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
CMD="${SSH_ORIGINAL_COMMAND:-}"
|
||||
|
||||
case "$CMD" in
|
||||
"rsync --server"*)
|
||||
exec rrsync "${APP_HOME}/staging"
|
||||
;;
|
||||
deploy-public)
|
||||
exec "${OPT_DIR}/deploy-public.sh"
|
||||
;;
|
||||
deploy-manager)
|
||||
exec "${OPT_DIR}/deploy-manager.sh"
|
||||
;;
|
||||
deploy-api)
|
||||
exec "${OPT_DIR}/deploy-api.sh"
|
||||
;;
|
||||
*)
|
||||
echo "ssh-wrapper: unknown command: ${CMD}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,37 @@
|
||||
[Unit]
|
||||
Description=DeepDrft API — dual-database authority (track metadata + FileDatabase + AuthBlocks)
|
||||
After=network-online.target postgresql.service
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
WorkingDirectory=%h/api/deepdrft/bin
|
||||
ExecStart=%h/api/deepdrft/bin/DeepDrftAPI
|
||||
|
||||
# Non-secret config — hardcoded; no plaintext file needed.
|
||||
Environment=ASPNETCORE_ENVIRONMENT=Production
|
||||
Environment=ASPNETCORE_URLS=http://localhost:5002
|
||||
|
||||
# Secrets — loaded at startup into $CREDENTIALS_DIRECTORY/.
|
||||
# Files live at %h/.config/credentials/ (deepdrft:deepdrft 600).
|
||||
#
|
||||
# LoadCredential ids (left of colon) MUST exactly match CredentialTools.ResolvePathOrThrow
|
||||
# keys in the application code. Wrong id -> service throws on startup.
|
||||
# filedatabase -> FileDatabaseSettings.VaultPath
|
||||
# apikey -> ApiKeySettings.ApiKey
|
||||
# connections -> ConnectionStrings.DefaultConnection + .Auth (two PG databases)
|
||||
# authblocks -> AuthBlocks JWT / Email / Admin config
|
||||
LoadCredential=filedatabase:%h/.config/credentials/filedatabase.json
|
||||
LoadCredential=apikey:%h/.config/credentials/apikey.json
|
||||
LoadCredential=connections:%h/.config/credentials/connections.json
|
||||
LoadCredential=authblocks:%h/.config/credentials/authblocks.json
|
||||
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=deepdrftapi
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -0,0 +1,30 @@
|
||||
[Unit]
|
||||
Description=DeepDrft Manager — Blazor InteractiveServer CMS
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
WorkingDirectory=%h/manager/bin
|
||||
ExecStart=%h/manager/bin/DeepDrftManager
|
||||
|
||||
# Non-secret config — hardcoded; no plaintext file needed.
|
||||
Environment=ASPNETCORE_ENVIRONMENT=Production
|
||||
Environment=ASPNETCORE_URLS=http://localhost:5001
|
||||
|
||||
# Secrets — loaded at startup into $CREDENTIALS_DIRECTORY/.
|
||||
# File lives at %h/.config/credentials/ (deepdrft:deepdrft 600).
|
||||
# LoadCredential id 'api' must match CredentialTools.ResolvePathOrThrow("api", ...) key.
|
||||
LoadCredential=api:%h/.config/credentials/api-manager.json
|
||||
|
||||
# Forward headers are configured in Program.cs for the nginx proxy topology.
|
||||
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=deepdrftmanager
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -0,0 +1,30 @@
|
||||
[Unit]
|
||||
Description=DeepDrft Public Site — Blazor SSR + WASM public listener site
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
WorkingDirectory=%h/public/bin
|
||||
ExecStart=%h/public/bin/DeepDrftPublic
|
||||
|
||||
# Non-secret config — hardcoded; no plaintext file needed.
|
||||
Environment=ASPNETCORE_ENVIRONMENT=Production
|
||||
Environment=ASPNETCORE_URLS=http://localhost:5000
|
||||
|
||||
# Secrets — loaded at startup into $CREDENTIALS_DIRECTORY/.
|
||||
# File lives at %h/.config/credentials/ (deepdrft:deepdrft 600).
|
||||
# LoadCredential id 'api' must match CredentialTools.ResolvePathOrThrow("api", ...) key.
|
||||
LoadCredential=api:%h/.config/credentials/api-public.json
|
||||
|
||||
# Forward headers are configured in Program.cs for the nginx proxy topology.
|
||||
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=deepdrftpublic
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
Reference in New Issue
Block a user