Author SHA1 Message Date
claude 1b74e230ad feat: 失敗セッションの調査性を改善(終了Pod保持 + ログAPI + UI)(v2.1.263-d)
Helm Chart Release / release-chart (push) Successful in 7s
Build Session Image / build-push (push) Successful in 2m3s
セッション Pod がブート失敗で Failed になると reaper が即削除し kubectl logs で
死因を追えなかった。「開く」で 502 になるだけで原因不明だった。

- session.reaper.terminatedRetentionSeconds(既定 900): 終了済み Pod を
  finishedAt からこの秒数は回収しない。manager 回収ループと reaper CronJob 両方が尊重
- GET /api/sessions/<id>/log: セッション Pod のログを text で返す(pods/log get のみ、追加権限なし)
- UI: 「ログ」ボタン、状態の色分け、Failed 行に reason/exitCode/message 併記、
  Running 以外は「開く」非表示
- session-entrypoint.sh: URL スキームと identity 型の不一致を警告。
  git clone を --progress ... 2>&1 にして git/ssh の出力を Pod ログへ
- helm lint / template 検証済み
2026-09-08 21:14:56 +09:00
10 changed files with 184 additions and 16 deletions
+38
View File
@@ -0,0 +1,38 @@
# CHANGELOG 2.1.263-d — 手動リリース
## 背景
セッション Pod がブート失敗(`git clone` エラー等)で `Failed` になると、reaper が
即座に削除するため `kubectl logs` で死因を確認できなかった。「開く」で 502 になるだけで
原因が分からない状態だった。
## 追加 / 変更
- **`session.reaper.terminatedRetentionSeconds`(既定 900)**
終了済み(Succeeded / Failed)のセッション Pod を、この秒数が経過するまで回収しない。
Pod の `containerStatuses[].state.terminated.finishedAt` を基準に判定。`0` で旧挙動(即回収)。
manager の回収ループと予備 CronJob の両方が尊重する。
- **`GET /api/sessions/<id>/log`**
セッション Pod(`claude` コンテナ)の直近ログを text/plain で返す。manager は既に
`pods/log` の `get` 権限を持つ(追加権限なし)。
- **Web UI**
- セッション一覧に **「ログ」ボタン**(インラインでログ表示)。
- 状態を色分け表示。`Failed` の行に `reason` / `exit <code>` / `message` を併記。
- `Running` 以外では「開く」を出さない。
- **セッションブートスクリプト(`files/session-entrypoint.sh`)**
- URL のスキーム(`https://` / `git@` / `ssh://`)と選択された identity の型
(`ssh` / `https-token`)の不一致を検出して警告。
- `git clone` を `--progress ... 2>&1` で実行し、git / ssh の出力をそのまま Pod ログへ。
失敗時は代表的な原因(認証不足・権限なし・known_hosts 不一致・NetworkPolicy による
egress 遮断)をメッセージで提示。
## トラブルシューティング追記(README)
- 「開く」で 502 / 一覧が `Failed` → UI の「ログ」ボタンで死因確認。
- clone が 60〜130 秒でタイムアウト → NetworkPolicy(Cilium 等)の egress 遮断を疑い、
`--set networkPolicy.enabled=false` で切り分け。
- URL スキームと identity 型を一致させる(HTTPS ↔ `https-token` / SSH ↔ `ssh`)。
## バージョン
`version` = `2.1.263-d` / `appVersion` = `"2.1.263"`。
+18
View File
@@ -57,6 +57,24 @@ Git リポジトリ(GitHub / Gitea など互換 Git 全般)をチェック
## 改修履歴 ## 改修履歴
### 失敗セッションの調査性改善(終了 Pod の保持 + ログ API + UI)(2026-09-08, v2.1.263-d)
**症状**: セッション Pod がブート失敗で `Failed` になると reaper が即削除し、`kubectl logs` で
死因を追えない。UI では「開く」で 502 になるだけ。
**修正内容**:
- `session.reaper.terminatedRetentionSeconds`(既定 900)を追加。終了済み Pod を
`finishedAt` からこの秒数は残す。`manager.py` の `reap_once` と reaper CronJob の両方が尊重。
- `GET /api/sessions/<id>/log`(`k8s_text` で pods/log を text 取得。追加 RBAC 不要)。
- UI: 「ログ」ボタン、状態の色分け、`Failed` 行に reason/exitCode/message 併記、
`Running` 以外は「開く」非表示。
- `session-entrypoint.sh`: URL スキームと identity 型の不一致を警告。`git clone` を
`--progress ... 2>&1` にして git/ssh 出力を Pod ログへ。
**対象ファイル**: `files/manager.py`, `files/session-entrypoint.sh`, `values.yaml`,
`values.schema.json`, `templates/deployment.yaml`, `templates/cronjob-reaper.yaml`,
`README.md`, `Chart.yaml`, `CHANGELOG-2.1.263-d.md`
### gateway nginx クラッシュループ修正(location 正規表現のクォート)(2026-09-08, v2.1.263-c) ### gateway nginx クラッシュループ修正(location 正規表現のクォート)(2026-09-08, v2.1.263-c)
**症状**: gateway コンテナが起動直後に **症状**: gateway コンテナが起動直後に
+1 -1
View File
@@ -6,7 +6,7 @@ description: >-
使い捨てコンテナで Git リポジトリ(GitHub / Gitea など)をチェックアウト・編集・ 使い捨てコンテナで Git リポジトリ(GitHub / Gitea など)をチェックアウト・編集・
検証・プッシュする。一定時間アイドルなセッションは自動破棄される。 検証・プッシュする。一定時間アイドルなセッションは自動破棄される。
type: application type: application
version: 2.1.263-c version: 2.1.263-d
appVersion: "2.1.263" appVersion: "2.1.263"
keywords: keywords:
- claude-code - claude-code
+6 -2
View File
@@ -489,10 +489,13 @@ docker buildx build --platform linux/amd64,linux/arm64 \
| 症状 | 原因と対処 | | 症状 | 原因と対処 |
|---|---| |---|---|
| UI が 502 / セッションを開くと 502 | セッション Pod がまだ Ready でない(初回は `npm i` に時間)。数十秒待つ。恒久対策は事前ビルドイメージ | | セッションを「開く」と 502 / 一覧で `Failed` になる | セッション Pod のブートが失敗している。UI の **「ログ」ボタン**(または `kubectl logs -n <sessionsNamespace> claude-sess-<id>`)で死因を確認。終了 Pod は `session.reaper.terminatedRetentionSeconds`(既定 900s)残る |
| clone が ~60〜130 秒後にタイムアウトして `Failed` | セッション Pod から Git ホストへの egress が届いていない。多くは **NetworkPolicy(Cilium 等)** が `ipBlock` egress を通していない。`--set networkPolicy.enabled=false` で切り分け。恒久対応は `networkPolicy.sessionEgressExtra` に対象 CIDR を追加、または Cilium 環境なら egress ルールの調整 |
| clone が `could not read Username` / `Authentication failed` | プライベートリポジトリに認証情報が無い。HTTPS URL には `https-token` identity(Gitea/GitHub の PAT)を、SSH URL(`git@host:...` / `ssh://git@host/...`)には `ssh` identity を選ぶ。**URL のスキームと identity の型を一致させる** |
| clone が `repository ... not found` / `does not appear to be a git repository` | パス誤り、またはその identity のアカウントに対象リポジトリの参照権限が無い |
| UI が 502(`/s/...` のログが gateway に出ない) | 前段(LB / Ingress)が実体の無いセッションに返している。まず一覧で該当セッションが `Running` か、上記でブート失敗していないか確認 |
| セッション作成が `許可されていない Git ホスト` | `git.allowedHosts` に対象ホストを追加(IP と FQDN は別物) | | セッション作成が `許可されていない Git ホスト` | `git.allowedHosts` に対象ホストを追加(IP と FQDN は別物) |
| clone が `Host key verification failed` | identity の `knownHosts` が対象ホストの鍵と不一致。`ssh-keyscan <host>` で取り直す | | clone が `Host key verification failed` | identity の `knownHosts` が対象ホストの鍵と不一致。`ssh-keyscan <host>` で取り直す |
| clone が `terminated ... could not read Username` | HTTPS の場合 `git.identities.<name>` 未指定 or token 無効。SSH の場合は identity の type/秘密鍵を確認 |
| WebSocket がすぐ切れる | Ingress の `proxy-read-timeout` を延長。`auth.mode: token` で Cookie 未設定なら一度 UI からログイン | | WebSocket がすぐ切れる | Ingress の `proxy-read-timeout` を延長。`auth.mode: token` で Cookie 未設定なら一度 UI からログイン |
| セッションが破棄されない | manager の RBAC / ログを確認。予備 CronJob(`session.reaper.cronjob`)が有効か | | セッションが破棄されない | manager の RBAC / ログを確認。予備 CronJob(`session.reaper.cronjob`)が有効か |
| `apk` エラーがログに出る | 既定イメージ + `readOnlyRootFilesystem` の制約。事前ビルドイメージを使う | | `apk` エラーがログに出る | 既定イメージ + `readOnlyRootFilesystem` の制約。事前ビルドイメージを使う |
@@ -525,6 +528,7 @@ docker buildx build --platform linux/amd64,linux/arm64 \
| バージョン | Claude Code | 変更内容 | | バージョン | Claude Code | 変更内容 |
|---|---|---| |---|---|---|
| 2.1.263-d | 2.1.263 | 失敗したセッションの調査性を改善。`session.reaper.terminatedRetentionSeconds`(既定 900)で終了 Pod を即削除せず保持。UI に **「ログ」ボタン**(`GET /api/sessions/<id>/log`)を追加し、`Failed` の理由(exitCode / reason)を一覧に表示。セッションブートスクリプトが URL スキームと identity 型の不一致を警告し、`git clone` の出力をそのままログに出すよう変更 |
| 2.1.263-c | 2.1.263 | 修正: gateway の nginx が起動時 `pcre2_compile() failed: missing closing parenthesis` でクラッシュループ。`location ~` の正規表現に `{10}` が含まれ、nginx がブロック区切りの `{}` と解釈していた。正規表現全体をダブルクォートで囲んで解消 | | 2.1.263-c | 2.1.263 | 修正: gateway の nginx が起動時 `pcre2_compile() failed: missing closing parenthesis` でクラッシュループ。`location ~` の正規表現に `{10}` が含まれ、nginx がブロック区切りの `{}` と解釈していた。正規表現全体をダブルクォートで囲んで解消 |
| 2.1.263-b | 2.1.263 | `anthropic.authMode: credentials`(既定)を追加。`~/.claude/.credentials.json` 全体を注入し、セッション内 `claude` の自己更新に加え、クラスタ内 refresher CronJob(`refreshToken` で Secret を定期更新)でトークン失効に自動追従。→ トークンが変わっても再デプロイ不要。refresher の RBAC は認証情報 Secret 1 個の get/patch に限定 | | 2.1.263-b | 2.1.263 | `anthropic.authMode: credentials`(既定)を追加。`~/.claude/.credentials.json` 全体を注入し、セッション内 `claude` の自己更新に加え、クラスタ内 refresher CronJob(`refreshToken` で Secret を定期更新)でトークン失効に自動追従。→ トークンが変わっても再デプロイ不要。refresher の RBAC は認証情報 Secret 1 個の get/patch に限定 |
| 2.1.263-a | 2.1.263 | Service を `LoadBalancer` 対応(kube-vip 等でローカル LB の IP から管理画面へアクセス、`loadBalancerSourceRanges` で接続元制限、既定を `LoadBalancer` に変更)。Ingress は従来どおり併用可。API を使わないサブスクリプション認証(`claude setup-token` → `authMode: oauth`)の手順を README に追記 | | 2.1.263-a | 2.1.263 | Service を `LoadBalancer` 対応(kube-vip 等でローカル LB の IP から管理画面へアクセス、`loadBalancerSourceRanges` で接続元制限、既定を `LoadBalancer` に変更)。Ingress は従来どおり併用可。API を使わないサブスクリプション認証(`claude setup-token` → `authMode: oauth`)の手順を README に追記 |
+90 -8
View File
@@ -64,6 +64,9 @@ GIT_AUTHOR_EMAIL = env("GIT_DEFAULT_AUTHOR_EMAIL", "claude@cafepieters.com")
IDLE_TIMEOUT = int(env("IDLE_TIMEOUT_SECONDS", "1800")) IDLE_TIMEOUT = int(env("IDLE_TIMEOUT_SECONDS", "1800"))
MAX_LIFETIME = int(env("MAX_LIFETIME_SECONDS", "43200")) MAX_LIFETIME = int(env("MAX_LIFETIME_SECONDS", "43200"))
REAP_INTERVAL = int(env("REAP_INTERVAL_SECONDS", "60")) REAP_INTERVAL = int(env("REAP_INTERVAL_SECONDS", "60"))
# 終了済み(Succeeded / Failed)セッション Pod を残す秒数。0 なら即回収。
# 死因を kubectl logs / UI「ログ」で調べられるよう既定で猶予を持たせる。
TERMINATED_RETENTION = int(env("TERMINATED_RETENTION_SECONDS", "900"))
MAX_SESSIONS = int(env("MAX_SESSIONS", "10")) MAX_SESSIONS = int(env("MAX_SESSIONS", "10"))
TTYD_PORT = int(env("TTYD_PORT", "7681")) TTYD_PORT = int(env("TTYD_PORT", "7681"))
SESSION_RESOURCES = env_json("SESSION_RESOURCES_JSON", {}) SESSION_RESOURCES = env_json("SESSION_RESOURCES_JSON", {})
@@ -146,6 +149,23 @@ def k8s(method, path, body=None, ctype="application/json"):
def core(path): def core(path):
return "/api/v1/namespaces/" + urllib.parse.quote(SESSIONS_NS) + path return "/api/v1/namespaces/" + urllib.parse.quote(SESSIONS_NS) + path
def k8s_text(path):
"""text/plain を返すエンドポイント(Pod ログ)用。(status, text) を返す。"""
req = urllib.request.Request(url=K8S_HOST + path, method="GET")
req.add_header("Authorization", "Bearer " + K8S_TOKEN)
try:
with urllib.request.urlopen(req, context=_ssl_ctx, timeout=30) as resp:
return resp.status, resp.read().decode("utf-8", "replace")
except urllib.error.HTTPError as exc:
return exc.code, exc.read().decode("utf-8", "replace")
def session_log(sid, tail=800):
if not SID_RE.match(sid):
return 400, "bad session id"
return k8s_text(core("/pods/claude-sess-" + sid + "/log?container=claude&tailLines=" + str(int(tail))))
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
# セッション操作 # セッション操作
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
@@ -166,7 +186,7 @@ def list_sessions():
meta = pod.get("metadata", {}) meta = pod.get("metadata", {})
ann = meta.get("annotations", {}) ann = meta.get("annotations", {})
st = pod.get("status", {}) st = pod.get("status", {})
out.append({ info = {
"id": meta.get("labels", {}).get(LABEL_SID, ""), "id": meta.get("labels", {}).get(LABEL_SID, ""),
"phase": st.get("phase", "Unknown"), "phase": st.get("phase", "Unknown"),
"repo": ann.get(ANN_PREFIX + "repo", ""), "repo": ann.get(ANN_PREFIX + "repo", ""),
@@ -175,10 +195,33 @@ def list_sessions():
"owner": ann.get(ANN_PREFIX + "owner", ""), "owner": ann.get(ANN_PREFIX + "owner", ""),
"createdAt": ann.get(ANN_PREFIX + "created-at", ""), "createdAt": ann.get(ANN_PREFIX + "created-at", ""),
"lastActivity": ann.get(ANN_PREFIX + "last-activity", ""), "lastActivity": ann.get(ANN_PREFIX + "last-activity", ""),
}) "reason": st.get("reason", ""), # DeadlineExceeded 等
"message": st.get("message", ""),
"finishedAt": "",
"exitCode": None,
}
for cs in st.get("containerStatuses", []) or []:
term = (cs.get("state") or {}).get("terminated")
if term:
info["finishedAt"] = term.get("finishedAt", "")
info["exitCode"] = term.get("exitCode")
if not info["reason"]:
info["reason"] = term.get("reason", "")
out.append(info)
return out return out
def _rfc3339_epoch(value):
"""RFC3339(...Z)→ epoch 秒。失敗時は 0。"""
if not value:
return 0
try:
import datetime
return int(datetime.datetime.fromisoformat(value.replace("Z", "+00:00")).timestamp())
except Exception: # noqa: BLE001
return 0
def active_session_count(): def active_session_count():
return sum(1 for s in list_sessions() if s["phase"] in ("Pending", "Running")) return sum(1 for s in list_sessions() if s["phase"] in ("Pending", "Running"))
@@ -434,6 +477,9 @@ def reap_once():
live_ids.add(sid) live_ids.add(sid)
reason = None reason = None
if s["phase"] in ("Succeeded", "Failed"): if s["phase"] in ("Succeeded", "Failed"):
# 終了済み Pod は TERMINATED_RETENTION の猶予後に回収(死因調査のため残す)
fin = _rfc3339_epoch(s.get("finishedAt")) or (int(s["createdAt"] or 0) + 120)
if now - fin >= TERMINATED_RETENTION:
reason = "phase=" + s["phase"] reason = "phase=" + s["phase"]
else: else:
try: try:
@@ -508,6 +554,10 @@ td,th{text-align:left;padding:.45rem .3rem;border-bottom:1px solid #8883;vertica
a.open{display:inline-block;background:#0891b2;color:#fff;padding:.3rem .7rem;border-radius:6px;text-decoration:none} a.open{display:inline-block;background:#0891b2;color:#fff;padding:.3rem .7rem;border-radius:6px;text-decoration:none}
.muted{opacity:.65;font-size:.85rem} .muted{opacity:.65;font-size:.85rem}
code{background:#8882;padding:.1rem .3rem;border-radius:4px} code{background:#8882;padding:.1rem .3rem;border-radius:4px}
.st-Running{color:#059669;font-weight:600}
.st-Failed{color:#dc2626;font-weight:600}
.st-Pending,.st-Succeeded{color:#d97706;font-weight:600}
pre#logbox{white-space:pre-wrap;background:#111;color:#eee;padding:.8rem;border-radius:8px;max-height:50vh;overflow:auto;font-size:.8rem}
</style></head><body> </style></head><body>
<h1>Claude Code — Remote Sessions</h1> <h1>Claude Code — Remote Sessions</h1>
<div id="login" hidden> <div id="login" hidden>
@@ -534,7 +584,14 @@ code{background:#8882;padding:.1rem .3rem;border-radius:4px}
</div> </div>
<table><thead><tr><th>ID</th><th>状態</th><th>リポジトリ</th><th></th></tr></thead> <table><thead><tr><th>ID</th><th>状態</th><th>リポジトリ</th><th></th></tr></thead>
<tbody id="rows"></tbody></table> <tbody id="rows"></tbody></table>
<p class="muted">アイドル <span id="idle"></span> 秒で自動破棄されます。</p> <p class="muted">アイドル <span id="idle"></span> 秒で自動破棄されます。終了したセッションは調査用にしばらく残ります。</p>
</div>
<div class="card" id="logcard" hidden>
<div class="row" style="justify-content:space-between">
<strong>ログ: <code id="logid"></code></strong>
<button class="sec" style="width:auto;margin:0" onclick="$('#logcard').hidden=true">閉じる</button>
</div>
<pre id="logbox"></pre>
</div> </div>
</div> </div>
<script> <script>
@@ -566,16 +623,34 @@ async function doCreate(e){e.preventDefault();
} }
async function del(id){if(!confirm('セッション '+id+' を破棄しますか?'))return; async function del(id){if(!confirm('セッション '+id+' を破棄しますか?'))return;
await api('/api/sessions/'+id,{method:'DELETE'});refresh();} await api('/api/sessions/'+id,{method:'DELETE'});refresh();}
async function showLog(id){
$('#logcard').hidden=false;$('#logid').textContent=id;
$('#logbox').textContent='読み込み中...';
const r=await api('/api/sessions/'+id+'/log');
$('#logbox').textContent=await r.text();
$('#logbox').scrollTop=$('#logbox').scrollHeight;
$('#logcard').scrollIntoView({behavior:'smooth'});
}
function esc(s){return (s||'').replace(/[&<>]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]));}
async function refresh(){ async function refresh(){
const r=await api('/api/sessions');if(!r.ok)return; const r=await api('/api/sessions');if(!r.ok)return;
const list=(await r.json()).sessions||[]; const list=(await r.json()).sessions||[];
$('#rows').innerHTML=list.map(s=>`<tr> $('#rows').innerHTML=list.map(s=>{
let sub=esc(s.ref);
if(s.phase==='Failed'||s.reason){
const d=[s.reason,(s.exitCode!=null?'exit '+s.exitCode:''),esc(s.message)].filter(Boolean).join(' / ');
if(d) sub+=` — <span class="st-Failed">${d}</span>`;
}
const canOpen=s.phase==='Running';
return `<tr>
<td><code>${s.id}</code></td> <td><code>${s.id}</code></td>
<td>${s.phase}</td> <td><span class="st-${s.phase}">${s.phase}</span></td>
<td style="word-break:break-all">${s.repo||''}<div class="muted">${s.ref||''}</div></td> <td style="word-break:break-all">${esc(s.repo)}<div class="muted">${sub}</div></td>
<td><a class="open" href="/s/${s.id}/" target="_blank" rel="noopener">開く</a> <td>${canOpen?`<a class="open" href="/s/${s.id}/" target="_blank" rel="noopener">開く</a> `:''}
<button class="sec" style="width:auto;margin:.3rem .3rem 0 0" onclick="showLog('${s.id}')">ログ</button>
<button class="sec" style="width:auto;margin:.3rem 0 0" onclick="del('${s.id}')">破棄</button></td> <button class="sec" style="width:auto;margin:.3rem 0 0" onclick="del('${s.id}')">破棄</button></td>
</tr>`).join('')||'<tr><td colspan=4 class="muted">セッションはありません</td></tr>'; </tr>`;
}).join('')||'<tr><td colspan=4 class="muted">セッションはありません</td></tr>';
} }
boot(); boot();
</script></body></html> </script></body></html>
@@ -648,6 +723,13 @@ class Handler(BaseHTTPRequestHandler):
if not token_ok(self): if not token_ok(self):
return self._json(401, {"error": "unauthorized"}) return self._json(401, {"error": "unauthorized"})
return self._json(200, {"sessions": list_sessions()}) return self._json(200, {"sessions": list_sessions()})
m = re.match(r"^/api/sessions/([a-f0-9]{10})/log$", path)
if m:
if not token_ok(self):
return self._json(401, {"error": "unauthorized"})
status, text = session_log(m.group(1))
return self._send(status if status == 200 else 200,
text or "(ログはまだありません)", "text/plain; charset=utf-8")
return self._send(404, b'{"error":"not found"}') return self._send(404, b'{"error":"not found"}')
def do_POST(self): def do_POST(self):
+19 -2
View File
@@ -86,13 +86,30 @@ esac
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
# 2. clone # 2. clone
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
# URL のスキームと identity の型が食い違っていないか確認(よくある詰まり)
case "$REPO_URL" in
https://*)
if [ "${GIT_IDENTITY_TYPE:-}" = "ssh" ]; then
err "HTTPS の URL ($REPO_URL) に SSH identity が選ばれています。"
err "→ プライベートリポジトリなら https-token の identity を選ぶか、URL を ssh://git@<host>/<org>/<repo>.git 形式にしてください。"
fi ;;
git@*|ssh://*)
if [ "${GIT_IDENTITY_TYPE:-}" != "ssh" ]; then
err "SSH の URL ($REPO_URL) に SSH identity が選ばれていません(identity=${GIT_IDENTITY_TYPE:-none})。"
err "→ ssh 型の identity(秘密鍵 + known_hosts)を選んでください。"
fi ;;
esac
REPO_DIR="$WORKSPACE_DIR/$(basename "${REPO_URL%.git}")" REPO_DIR="$WORKSPACE_DIR/$(basename "${REPO_URL%.git}")"
if [ -d "$REPO_DIR/.git" ]; then if [ -d "$REPO_DIR/.git" ]; then
log "既存チェックアウトを再利用: $REPO_DIR" log "既存チェックアウトを再利用: $REPO_DIR"
else else
log "clone 中: $REPO_URL ($REPO_REF)" log "clone 中: $REPO_URL ($REPO_REF)"
if ! git clone --filter=blob:none --no-tags "$REPO_URL" "$REPO_DIR"; then # 進捗と失敗理由をそのまま Pod ログに出す(git のメッセージが死因の手がかり)
err "clone に失敗しました"; exit 1 if ! GIT_SSH_COMMAND="${GIT_SSH_COMMAND:-ssh} -v" git clone --progress --filter=blob:none --no-tags "$REPO_URL" "$REPO_DIR" 2>&1; then
err "clone に失敗しました(上の git / ssh の出力を参照)。"
err "よくある原因: 認証情報の不足・権限なし / known_hosts 不一致 / NetworkPolicy による egress 遮断(接続タイムアウト)。"
exit 1
fi fi
fi fi
cd "$REPO_DIR" cd "$REPO_DIR"
+2
View File
@@ -46,6 +46,8 @@ spec:
value: {{ .Values.session.idleTimeoutSeconds | quote }} value: {{ .Values.session.idleTimeoutSeconds | quote }}
- name: MAX_LIFETIME_SECONDS - name: MAX_LIFETIME_SECONDS
value: {{ .Values.session.maxLifetimeSeconds | quote }} value: {{ .Values.session.maxLifetimeSeconds | quote }}
- name: TERMINATED_RETENTION_SECONDS
value: {{ .Values.session.reaper.terminatedRetentionSeconds | quote }}
- name: TTYD_PORT - name: TTYD_PORT
value: {{ .Values.session.ttydPort | quote }} value: {{ .Values.session.ttydPort | quote }}
securityContext: securityContext:
+2
View File
@@ -151,6 +151,8 @@ spec:
value: {{ .Values.session.maxLifetimeSeconds | quote }} value: {{ .Values.session.maxLifetimeSeconds | quote }}
- name: REAP_INTERVAL_SECONDS - name: REAP_INTERVAL_SECONDS
value: {{ .Values.session.reaper.intervalSeconds | quote }} value: {{ .Values.session.reaper.intervalSeconds | quote }}
- name: TERMINATED_RETENTION_SECONDS
value: {{ .Values.session.reaper.terminatedRetentionSeconds | quote }}
- name: MAX_SESSIONS - name: MAX_SESSIONS
value: {{ .Values.session.maxSessions | quote }} value: {{ .Values.session.maxSessions | quote }}
- name: TTYD_PORT - name: TTYD_PORT
+1
View File
@@ -180,6 +180,7 @@
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"intervalSeconds": { "type": "integer", "minimum": 10 }, "intervalSeconds": { "type": "integer", "minimum": 10 },
"terminatedRetentionSeconds": { "type": "integer", "minimum": 0 },
"cronjob": { "cronjob": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
+4
View File
@@ -268,6 +268,10 @@ session:
reaper: reaper:
# manager 内アイドル回収ループの実行間隔 # manager 内アイドル回収ループの実行間隔
intervalSeconds: 60 intervalSeconds: 60
# 終了済み(Succeeded / Failed)セッション Pod を回収せず残す秒数。
# 死因を UI の「ログ」ボタンや kubectl logs で確認できるよう猶予を持たせる。
# 0 にすると即回収(旧挙動)。
terminatedRetentionSeconds: 900
# 予備の孤児掃除 CronJob(manager 障害時の保険) # 予備の孤児掃除 CronJob(manager 障害時の保険)
cronjob: cronjob:
enabled: true enabled: true