トークンが定期的に失効・ローテーションしても再デプロイ不要にする。 - anthropic.authMode: credentials(新既定) ~/.claude/.credentials.json 全体(refreshToken 込み)を Secret に格納。 セッション Pod にはファイルとしてマウントし $HOME/.claude へコピー(書き込み可)、 セッション内 claude が refreshToken で自己更新 - anthropic.refresher CronJob(credentials 時・既定有効, files/token-refresher.py) Secret 内 refreshToken で OAuth トークンを更新し Secret を書き戻す。 専用 SA + resourceNames 限定 Role で該当 Secret の get/patch のみ。 tokenUrl/clientId は Claude Code CLI の非公式値(values 上書き可) - manager.py build_pod: credentials は envFrom を使わずファイルマウント + SEED_CREDENTIALS_FILE。apiKey/oauth は従来どおり envFrom - Secret に helm.sh/resource-policy: keep(credentials 時)で refresher の書込を保持 - fix: values.schema.json の $schema 行を削除(一部の Helm で "no schema with key or ref https://json-schema.org/draft-07/schema#" エラー) - helm lint / template(credentials/apiKey/oauth)検証済み
250 lines
11 KiB
Bash
250 lines
11 KiB
Bash
#!/bin/sh
|
||
# ==========================================================================
|
||
# claude-code セッションブート(使い捨てコンテナ内で1回だけ実行)
|
||
#
|
||
# 1. git 認証(SSH 鍵 / HTTPS トークン)を $HOME にセットアップ
|
||
# 2. 許可済みリポジトリを $WORKSPACE_DIR へ clone
|
||
# 3. `claude` を PATH に用意(無ければ非 root で $HOME にインストール)
|
||
# 4. ttyd でブラウザにシェル(+ 任意で claude 自動起動)を提供
|
||
# 5. アイドルウォッチドッグ:無通信が続いたら自身を終了 → Pod 破棄
|
||
#
|
||
# 前提: 非 root(uid=10001) / readOnlyRootFilesystem。
|
||
# 書き込み可能なのは $HOME, $WORKSPACE_DIR, /tmp のみ。
|
||
# git / ttyd / node は事前ビルドイメージに同梱されている想定
|
||
# (files/Dockerfile.session)。未同梱時は下記フォールバックが走る。
|
||
# ==========================================================================
|
||
set -eu
|
||
|
||
log() { printf '\033[36m[boot]\033[0m %s\n' "$*"; }
|
||
err() { printf '\033[31m[boot:ERROR]\033[0m %s\n' "$*" >&2; }
|
||
|
||
: "${HOME:=/home/claude}"
|
||
: "${WORKSPACE_DIR:=/workspace}"
|
||
: "${TTYD_PORT:=7681}"
|
||
: "${IDLE_TIMEOUT_SECONDS:=1800}"
|
||
: "${AUTOSTART_CLAUDE:=true}"
|
||
: "${REPO_REF:=main}"
|
||
export HOME
|
||
export NPM_CONFIG_CACHE="$HOME/.npm"
|
||
export PATH="$HOME/.npm-global/bin:$HOME/.local/bin:$PATH"
|
||
|
||
mkdir -p "$HOME/.ssh" "$WORKSPACE_DIR" "$HOME/.npm-global"
|
||
chmod 700 "$HOME/.ssh"
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 1. git 認証セットアップ
|
||
# --------------------------------------------------------------------------
|
||
git config --global user.name "${GIT_AUTHOR_NAME:-Claude}"
|
||
git config --global user.email "${GIT_AUTHOR_EMAIL:-claude@cafepieters.com}"
|
||
git config --global safe.directory '*'
|
||
git config --global advice.detachedHead false
|
||
git config --global credential.helper ''
|
||
|
||
ASKPASS_DISABLE() { export GIT_TERMINAL_PROMPT=0; }
|
||
ASKPASS_DISABLE
|
||
|
||
case "${GIT_IDENTITY_TYPE:-}" in
|
||
ssh)
|
||
if [ ! -r /git-identity/id ]; then err "SSH 鍵がマウントされていません"; exit 1; fi
|
||
install -m 600 /git-identity/id "$HOME/.ssh/id_session"
|
||
# known_hosts を必須にし、ホスト鍵の TOFU を無効化する
|
||
if [ -r /git-identity/known_hosts ]; then
|
||
install -m 644 /git-identity/known_hosts "$HOME/.ssh/known_hosts"
|
||
else
|
||
err "known_hosts がありません(StrictHostKeyChecking を維持するため必須)"; exit 1
|
||
fi
|
||
cat > "$HOME/.ssh/config" <<EOF
|
||
Host *
|
||
IdentityFile $HOME/.ssh/id_session
|
||
IdentitiesOnly yes
|
||
StrictHostKeyChecking yes
|
||
UserKnownHostsFile $HOME/.ssh/known_hosts
|
||
PasswordAuthentication no
|
||
EOF
|
||
chmod 600 "$HOME/.ssh/config"
|
||
export GIT_SSH_COMMAND="ssh -F $HOME/.ssh/config"
|
||
log "SSH 認証を構成しました"
|
||
;;
|
||
https-token)
|
||
if [ ! -r /git-identity/token ]; then err "HTTPS トークンがマウントされていません"; exit 1; fi
|
||
_u="${GIT_HTTPS_USERNAME:-x-access-token}"
|
||
_t="$(cat /git-identity/token)"
|
||
# トークンをプロセス環境にもファイルにも平文で残さず、credential helper で都度供給
|
||
cat > "$HOME/.git-credentials" <<EOF
|
||
https://$(printf '%s' "$_u" | sed 's/@/%40/g'):$(printf '%s' "$_t" | sed 's/@/%40/g')@$(printf '%s\n' "$REPO_URL" | sed -E 's#^https://([^/]+)/.*#\1#')
|
||
EOF
|
||
chmod 600 "$HOME/.git-credentials"
|
||
git config --global credential.helper "store --file=$HOME/.git-credentials"
|
||
unset _t
|
||
log "HTTPS トークン認証を構成しました"
|
||
;;
|
||
*)
|
||
log "git 認証なし(公開リポジトリのみ clone 可能)"
|
||
;;
|
||
esac
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 2. clone
|
||
# --------------------------------------------------------------------------
|
||
REPO_DIR="$WORKSPACE_DIR/$(basename "${REPO_URL%.git}")"
|
||
if [ -d "$REPO_DIR/.git" ]; then
|
||
log "既存チェックアウトを再利用: $REPO_DIR"
|
||
else
|
||
log "clone 中: $REPO_URL ($REPO_REF)"
|
||
if ! git clone --filter=blob:none --no-tags "$REPO_URL" "$REPO_DIR"; then
|
||
err "clone に失敗しました"; exit 1
|
||
fi
|
||
fi
|
||
cd "$REPO_DIR"
|
||
git checkout "$REPO_REF" 2>/dev/null || git checkout -b "$REPO_REF" 2>/dev/null || true
|
||
log "作業ディレクトリ: $REPO_DIR ($(git rev-parse --short HEAD 2>/dev/null || echo '?'))"
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 3. claude CLI を用意
|
||
# --------------------------------------------------------------------------
|
||
if ! command -v claude >/dev/null 2>&1; then
|
||
log "claude が未同梱のため $HOME にインストールします(初回は時間がかかります)"
|
||
npm config set prefix "$HOME/.npm-global" >/dev/null 2>&1 || true
|
||
_ver="${CLAUDE_CODE_VERSION:-}"
|
||
_pkg="@anthropic-ai/claude-code${_ver:+@$_ver}"
|
||
if ! npm install -g --no-fund --no-audit "$_pkg"; then
|
||
err "claude-code のインストールに失敗しました。事前ビルドイメージの利用を推奨します。"
|
||
exit 1
|
||
fi
|
||
fi
|
||
log "claude: $(claude --version 2>/dev/null || echo 'unknown')"
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 3b. Claude 認証情報(credentials モード)
|
||
# --------------------------------------------------------------------------
|
||
# Secret からマウントした .credentials.json を書き込み可能な $HOME/.claude へコピーする。
|
||
# claude はアクセストークン失効時に refreshToken でこのファイルを自己更新する
|
||
# (セッション中のみ有効。クラスタ全体の更新は refresher CronJob が担当)。
|
||
if [ -n "${SEED_CREDENTIALS_FILE:-}" ] && [ -r "$SEED_CREDENTIALS_FILE" ]; then
|
||
mkdir -p "$HOME/.claude"
|
||
chmod 700 "$HOME/.claude"
|
||
install -m 600 "$SEED_CREDENTIALS_FILE" "$HOME/.claude/.credentials.json"
|
||
if command -v jq >/dev/null 2>&1; then
|
||
_exp="$(jq -r '.claudeAiOauth.expiresAt // empty' "$HOME/.claude/.credentials.json" 2>/dev/null)"
|
||
[ -n "$_exp" ] && log "Claude 認証情報を配置(accessToken 期限: $(date -u -d "@$(( _exp / 1000 ))" 2>/dev/null || echo "$_exp"))"
|
||
else
|
||
log "Claude 認証情報を $HOME/.claude/.credentials.json に配置しました"
|
||
fi
|
||
unset ANTHROPIC_API_KEY CLAUDE_CODE_OAUTH_TOKEN
|
||
fi
|
||
|
||
# 追加 apk(root 権限がある事前ビルドイメージのみ有効。非 root では警告のみ)
|
||
if [ -n "${EXTRA_APK_PACKAGES:-}" ]; then
|
||
if [ "$(id -u)" = "0" ]; then
|
||
apk add --no-cache $EXTRA_APK_PACKAGES || err "apk add に一部失敗"
|
||
else
|
||
err "EXTRA_APK_PACKAGES は非 root では無視されます: $EXTRA_APK_PACKAGES"
|
||
fi
|
||
fi
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 4. 補助コマンド(claude-verify / claude-push)
|
||
# --------------------------------------------------------------------------
|
||
mkdir -p "$HOME/.local/bin"
|
||
|
||
cat > "$HOME/.local/bin/claude-verify" <<'EOF'
|
||
#!/bin/sh
|
||
# セッション設定の検証コマンドを実行する
|
||
if [ -z "${VERIFY_COMMAND:-}" ]; then echo "verifyCommand は未設定です"; exit 0; fi
|
||
echo "+ $VERIFY_COMMAND"
|
||
exec sh -c "$VERIFY_COMMAND"
|
||
EOF
|
||
chmod +x "$HOME/.local/bin/claude-verify"
|
||
|
||
cat > "$HOME/.local/bin/claude-push" <<'EOF'
|
||
#!/bin/sh
|
||
# 検証 → 安全なブランチへ push(デフォルト/保護ブランチ・--force は禁止)
|
||
set -eu
|
||
if [ "${AUTOPUSH_ENABLED:-false}" != "true" ]; then
|
||
echo "autoPush は無効です(values: session.autoPush.enabled)。手動 push は 'git push' を使ってください。" >&2
|
||
exit 1
|
||
fi
|
||
cur="$(git rev-parse --abbrev-ref HEAD)"
|
||
prefix="${AUTOPUSH_BRANCH_PREFIX:-claude/}"
|
||
protected="${AUTOPUSH_PROTECTED:-main,master,develop,release}"
|
||
case ",$protected," in *",$cur,"*)
|
||
echo "現在のブランチ '$cur' は保護対象です。作業ブランチへ切り替えてください。" >&2; exit 1;;
|
||
esac
|
||
case "$cur" in "$prefix"*) target="$cur" ;; *) target="${prefix}${cur}" ;; esac
|
||
if [ -n "${VERIFY_COMMAND:-}" ]; then
|
||
echo "== verify: $VERIFY_COMMAND"
|
||
sh -c "$VERIFY_COMMAND" || { echo "検証に失敗したため push を中止します。" >&2; exit 1; }
|
||
fi
|
||
if [ -n "$(git status --porcelain)" ]; then
|
||
git add -A
|
||
git commit -m "${1:-chore: work from claude-code session $SESSION_ID}"
|
||
fi
|
||
[ "$target" = "$cur" ] || git branch -f "$target" "$cur"
|
||
echo "== push origin $target(--force なし)"
|
||
exec git push origin "HEAD:$target"
|
||
EOF
|
||
chmod +x "$HOME/.local/bin/claude-push"
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 5. アイドルウォッチドッグ
|
||
# --------------------------------------------------------------------------
|
||
idle_watchdog() {
|
||
# ttyd への ESTABLISHED 接続が IDLE_TIMEOUT_SECONDS の間ゼロなら Pod を終了させる
|
||
last_seen="$(date +%s)"
|
||
while true; do
|
||
sleep 30
|
||
conns=0
|
||
if command -v ss >/dev/null 2>&1; then
|
||
conns="$(ss -Htn "sport = :$TTYD_PORT" state established 2>/dev/null | wc -l | tr -d ' ')"
|
||
elif [ -r /proc/net/tcp ]; then
|
||
hexport="$(printf '%04X' "$TTYD_PORT")"
|
||
conns="$(awk -v p=":$hexport" '$2 ~ p"$" && $4=="01"{n++} END{print n+0}' /proc/net/tcp)"
|
||
fi
|
||
now="$(date +%s)"
|
||
if [ "${conns:-0}" -gt 0 ]; then
|
||
last_seen="$now"
|
||
elif [ $(( now - last_seen )) -ge "$IDLE_TIMEOUT_SECONDS" ]; then
|
||
err "アイドル ${IDLE_TIMEOUT_SECONDS}s を超過。セッションを終了します。"
|
||
kill -TERM "$TTYD_PID" 2>/dev/null || true
|
||
exit 0
|
||
fi
|
||
done
|
||
}
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 6. ttyd 起動
|
||
# --------------------------------------------------------------------------
|
||
if ! command -v ttyd >/dev/null 2>&1; then
|
||
err "ttyd が見つかりません。事前ビルドイメージ(files/Dockerfile.session)を利用してください。"
|
||
exit 1
|
||
fi
|
||
|
||
SHELL_CMD="/bin/bash"
|
||
command -v bash >/dev/null 2>&1 || SHELL_CMD="/bin/sh"
|
||
|
||
cat > "$HOME/.session-shell" <<EOF
|
||
#!/bin/sh
|
||
cd "$REPO_DIR" || cd "$WORKSPACE_DIR"
|
||
export PATH="$PATH"
|
||
echo "── Claude Code セッション $SESSION_ID ──────────────────────────"
|
||
echo "リポジトリ : $REPO_URL ($REPO_REF)"
|
||
echo "検証コマンド: ${VERIFY_COMMAND:-(未設定)} 補助: claude-verify / claude-push"
|
||
echo "─────────────────────────────────────────────────────────────"
|
||
if [ "${AUTOSTART_CLAUDE}" = "true" ] && command -v claude >/dev/null 2>&1; then
|
||
exec claude
|
||
fi
|
||
exec $SHELL_CMD -l
|
||
EOF
|
||
chmod +x "$HOME/.session-shell"
|
||
|
||
log "ttyd を 0.0.0.0:$TTYD_PORT で起動します"
|
||
ttyd --port "$TTYD_PORT" --interface 0.0.0.0 --writable \
|
||
--client-option fontSize=14 --client-option 'theme={"background":"#1e1e1e"}' \
|
||
--terminal-type xterm-256color \
|
||
"$HOME/.session-shell" &
|
||
TTYD_PID=$!
|
||
|
||
idle_watchdog &
|
||
|
||
wait "$TTYD_PID"
|