Compare commits
4
Commits
v2.1.263-d
...
v2.1.278
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1ef5a7310 | ||
|
|
351df17133 | ||
|
|
c31dd5875c | ||
|
|
6514747b4e |
@@ -0,0 +1,30 @@
|
||||
# CHANGELOG 2.1.263-e — 手動リリース
|
||||
|
||||
## 背景
|
||||
|
||||
`claude@192.168.9.65:websites/e-stay-tours.git`(SSH ユーザーを自分のアカウント名にした URL)が
|
||||
`リポジトリ URL の形式が不正です` で弾かれ、理由が分かりにくかった。
|
||||
Gitea・GitHub の SSH ユーザーは常に `git` だが、それを案内していなかった。
|
||||
|
||||
## 変更
|
||||
|
||||
- **`REPO_RE`(manager の URL バリデーション)を緩和**
|
||||
- `ssh://<user>@<host>[:port]/<path>` … `git` 以外のユーザーも受理
|
||||
- `<user>@<host>:<path>` … SCP 記法。`git@` 固定をやめて任意ユーザー可
|
||||
- `<host>:<path>` … ユーザー省略も受理
|
||||
- `//` で始まるパスは引き続き拒否(URL 誤入力の検出)
|
||||
- ホスト allowlist(`git.allowedHosts`)は従来どおり全形式に適用
|
||||
- **エラーメッセージを具体化**
|
||||
- URL 形式エラー時に受理される 3 形式と「SSH ユーザーは常に `git`」を明記
|
||||
- ホスト不許可時に許可済みホスト一覧を併記
|
||||
- **Web UI**: リポジトリ URL 入力欄の下に形式ヒントを表示
|
||||
|
||||
## 注意
|
||||
|
||||
`claude@…` のような不正ユーザーでも URL 形式としては通るようになったが、その場合の
|
||||
`git clone` は Gitea 側で失敗する。失敗ログは `2.1.263-d` で追加した「ログ」ボタンで確認できる。
|
||||
正しくは `git@<host>:<org>/<repo>.git`。
|
||||
|
||||
## バージョン
|
||||
|
||||
`version` = `2.1.263-e` / `appVersion` = `"2.1.263"`。
|
||||
@@ -0,0 +1,36 @@
|
||||
# CHANGELOG 2.1.263-f — 手動リリース
|
||||
|
||||
## 追加: GUI からの git identity 登録
|
||||
|
||||
Web UI の **「認証情報(identity)」カード** から、`helm` を触らずに SSH 秘密鍵 +
|
||||
`known_hosts` / HTTPS トークンを登録・削除できるようにした。
|
||||
|
||||
- `git.guiIdentities.enabled`(既定 `true`)
|
||||
- API:
|
||||
- `GET /api/identities` … Helm 管理 + GUI 登録の identity 一覧(`{name, type, source}`)
|
||||
- `POST /api/identities` … 登録(`{name, type, privateKey, knownHosts}` または
|
||||
`{name, type, username, token}`)。`ssh` は `known_hosts` 必須、秘密鍵の簡易検証あり。
|
||||
Helm 管理と同名は拒否。既存 GUI 分は置き換え。
|
||||
- `DELETE /api/identities/<name>` … 削除(`identity-source=gui` のみ。Helm 管理は不可)
|
||||
- 保存先: セッション NS の Secret。ラベル
|
||||
`claude-code.cafepieters.com/identity-source: gui|helm` /
|
||||
`claude-code.cafepieters.com/identity-type: ssh|https-token` で区別。
|
||||
- `build_pod` / セッション作成のバリデーションは Helm 管理と GUI 登録の両方を解決する
|
||||
(`resolve_identity` / `list_identities`)。
|
||||
- `/api/config` の `identities` は文字列配列 → `{name,type,source}` 配列に変更。UI も追従。
|
||||
|
||||
## RBAC
|
||||
|
||||
`git.guiIdentities.enabled: true` のとき、manager Role にセッション NS の
|
||||
`secrets` `["get","list","create","update","patch","delete"]` を追加(`false` なら付かない)。
|
||||
同 NS には Claude 認証情報 Secret も存在するため、不要なら無効化を推奨。
|
||||
|
||||
## セキュリティ注意
|
||||
|
||||
秘密鍵はブラウザ → gateway → manager をリクエストボディで通る。経路の TLS
|
||||
(Ingress の `tls` / LB の TLS 終端)を有効にすること。README「git — 接続先と認証
|
||||
アイデンティティ」に明記。
|
||||
|
||||
## バージョン
|
||||
|
||||
`version` = `2.1.263-f` / `appVersion` = `"2.1.263"`。
|
||||
@@ -57,6 +57,39 @@ Git リポジトリ(GitHub / Gitea など互換 Git 全般)をチェック
|
||||
|
||||
## 改修履歴
|
||||
|
||||
### GUI からの git identity 登録(2026-09-09, v2.1.263-f)
|
||||
|
||||
**要望**: SSH 鍵を `helm --set-file` ではなく Web UI から登録したい。
|
||||
|
||||
**修正内容**:
|
||||
- `git.guiIdentities.enabled`(既定 `true`)。UI に「認証情報」カード(一覧 + 追加フォーム +
|
||||
削除)。`GET/POST/DELETE /api/identities`。
|
||||
- 保存先はセッション NS の Secret(ラベル `identity-source: gui|helm` /
|
||||
`identity-type: ssh|https-token`)。`secret-git.yaml` の Helm 管理分にも同ラベルを付与。
|
||||
- `manager.py`: `resolve_identity` / `list_identities` / `create_gui_identity` /
|
||||
`delete_gui_identity` を追加。`build_pod` と `create_session` は Helm 管理 + GUI 登録の
|
||||
両方を解決。`/api/config` の `identities` を `{name,type,source}` 配列に変更(UI も追従)。
|
||||
- `rbac.yaml`: `guiIdentities.enabled` 時のみ manager Role に同 NS `secrets` の
|
||||
get/list/create/update/patch/delete を追加。
|
||||
- 秘密鍵はリクエストボディで manager へ届く → README に「経路の TLS 必須」を明記。
|
||||
|
||||
**対象ファイル**: `files/manager.py`, `values.yaml`, `values.schema.json`,
|
||||
`templates/rbac.yaml`, `templates/secret-git.yaml`, `templates/deployment.yaml`,
|
||||
`README.md`, `Chart.yaml`, `CHANGELOG-2.1.263-f.md`
|
||||
|
||||
### リポジトリ URL バリデーション緩和 + エラーメッセージ具体化(2026-09-09, v2.1.263-e)
|
||||
|
||||
**症状**: `claude@192.168.9.65:...`(SSH ユーザーをアカウント名にした URL)が
|
||||
`リポジトリ URL の形式が不正です` で弾かれ、原因が分かりにくい。Gitea/GitHub の SSH
|
||||
ユーザーは常に `git`。
|
||||
|
||||
**修正内容**: `manager.py` の `REPO_RE` を `ssh://<user>@host/…` / `<user>@host:path` /
|
||||
`host:path` も受理するよう緩和(`//` 始まりは拒否、ホスト allowlist は全形式に適用)。
|
||||
URL 形式エラーで受理形式と「SSH ユーザーは常に `git`」を明記、ホスト不許可で許可済み
|
||||
一覧を併記。UI 入力欄にヒント追加。README の git セクションに URL 形式表を追加。
|
||||
|
||||
**対象ファイル**: `files/manager.py`, `README.md`, `Chart.yaml`, `CHANGELOG-2.1.263-e.md`
|
||||
|
||||
### 失敗セッションの調査性改善(終了 Pod の保持 + ログ API + UI)(2026-09-08, v2.1.263-d)
|
||||
|
||||
**症状**: セッション Pod がブート失敗で `Failed` になると reaper が即削除し、`kubectl logs` で
|
||||
|
||||
+2
-2
@@ -6,8 +6,8 @@ description: >-
|
||||
使い捨てコンテナで Git リポジトリ(GitHub / Gitea など)をチェックアウト・編集・
|
||||
検証・プッシュする。一定時間アイドルなセッションは自動破棄される。
|
||||
type: application
|
||||
version: 2.1.263-d
|
||||
appVersion: "2.1.263"
|
||||
version: 2.1.278
|
||||
appVersion: "2.1.278"
|
||||
keywords:
|
||||
- claude-code
|
||||
- claude
|
||||
|
||||
@@ -401,7 +401,8 @@ Secret は **セッション NS** に作成されます。`credentials` モー
|
||||
| `git.allowedHosts` | GitHub/GitLab/Gitea | clone を許可するホスト allow-list |
|
||||
| `git.allowArbitraryRepos` | `false` | allow-list を無効化し任意 URL を許可(非推奨) |
|
||||
| `git.defaultAuthorName` / `Email` | `Claude` / `claude@cafepieters.com` | コミット author 既定 |
|
||||
| `git.identities.<name>.type` | — | `ssh` / `https-token` |
|
||||
| `git.guiIdentities.enabled` | `true` | **Web UI から identity を登録・削除できるようにする**(下記) |
|
||||
| `git.identities.<name>.type` | — | `ssh` / `https-token`(Helm 管理・UI では読み取り専用) |
|
||||
| `…ssh.sshPrivateKey` / `knownHosts` | — | PEM 秘密鍵 / `ssh-keyscan` 出力(**`knownHosts` 必須**) |
|
||||
| `…https-token.username` / `token` | `x-access-token` / — | HTTPS ベーシック(GitHub PAT は username 任意) |
|
||||
| `…existingSecret` ほか | — | 既存 Secret 参照(キー名は `existingSecretKey*` で調整) |
|
||||
@@ -409,6 +410,32 @@ Secret は **セッション NS** に作成されます。`credentials` モー
|
||||
GitHub でも Gitea でも GitLab でも、SSH か HTTPS トークンで認証できる Git ホストなら同じ仕組みで扱えます。
|
||||
セッション作成時に identity 名を選ぶと、その Secret だけがそのセッション Pod にマウントされます。
|
||||
|
||||
#### GUI からの identity 登録(`git.guiIdentities.enabled: true`)
|
||||
|
||||
Web UI の **「認証情報(identity)」カード** から、SSH 秘密鍵 + `known_hosts`、または
|
||||
HTTPS トークンを登録・削除できます。`helm` を触らずにセッションで使う鍵を追加できます。
|
||||
|
||||
- 登録内容はセッション NS の Secret(ラベル `claude-code.cafepieters.com/identity-source: gui`)
|
||||
として保存され、`git.identities`(Helm 管理・`identity-source: helm`)と同じ一覧に並びます。
|
||||
Helm 管理のものは UI から削除できません。
|
||||
- **セキュリティ**: 秘密鍵はブラウザ → gateway → manager をリクエストボディで通ります。
|
||||
**経路の TLS(Ingress の `tls` / LB の TLS 終端)を必ず有効に**してください。
|
||||
有効化すると manager にセッション NS の `secrets` への `get`/`list`/`create`/`update`/`delete`
|
||||
権限が付きます(同 NS には Claude 認証情報 Secret も存在)。不要なら `enabled: false`。
|
||||
- `known_hosts` は登録時必須です(`ssh-keyscan <host>` の出力を貼り付け。ホスト鍵 TOFU 無効化)。
|
||||
|
||||
**URL の形式**(`git.identities` で選んだ鍵で認証されます):
|
||||
|
||||
| 形式 | 例 | identity |
|
||||
|---|---|---|
|
||||
| SSH(SCP 記法) | `git@192.168.9.65:websites/e-stay-tours.git` | `ssh` 型 |
|
||||
| SSH(URL 記法) | `ssh://git@192.168.9.65/websites/e-stay-tours.git` | `ssh` 型 |
|
||||
| HTTPS | `https://git.cafepieters.com/websites/e-stay-tours.git` | `https-token` 型(プライベート時) |
|
||||
|
||||
> SSH のユーザー名は **常に `git`** です(Gitea・GitHub 共通)。自分の Gitea アカウント名
|
||||
> (`claude@…` など)ではありません。認証は identity の鍵で行われるため、その鍵を持つ
|
||||
> アカウントが対象リポジトリの参照権限を持っている必要があります。
|
||||
|
||||
### session — 使い捨てセッション
|
||||
|
||||
| キー | 既定 | 説明 |
|
||||
@@ -528,6 +555,10 @@ docker buildx build --platform linux/amd64,linux/arm64 \
|
||||
|
||||
| バージョン | Claude Code | 変更内容 |
|
||||
|---|---|---|
|
||||
| 2.1.278 | 2.1.278 | 自動更新 |
|
||||
| 2.1.270 | 2.1.270 | 自動更新 |
|
||||
| 2.1.263-f | 2.1.263 | **Web UI から git identity(SSH 秘密鍵 + known_hosts / HTTPS トークン)を登録・削除**できる「認証情報」カードを追加(`git.guiIdentities.enabled`、既定 `true`)。`GET/POST/DELETE /api/identities`。GUI 登録分はセッション NS の Secret(`identity-source: gui`)として保存し、Helm 管理分と同じ一覧に統合。manager RBAC に同 NS の `secrets` CRUD を条件付き付与 |
|
||||
| 2.1.263-e | 2.1.263 | リポジトリ URL バリデーションを緩和(`ssh://user@…` / `user@host:path` / `host:path` も受理。ホスト allowlist は従来どおり適用)。エラーメッセージに受理形式と「SSH ユーザーは常に `git`」の注意を明記。UI の入力欄にヒントを追加 |
|
||||
| 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-b | 2.1.263 | `anthropic.authMode: credentials`(既定)を追加。`~/.claude/.credentials.json` 全体を注入し、セッション内 `claude` の自己更新に加え、クラスタ内 refresher CronJob(`refreshToken` で Secret を定期更新)でトークン失効に自動追従。→ トークンが変わっても再デプロイ不要。refresher の RBAC は認証情報 Secret 1 個の get/patch に限定 |
|
||||
|
||||
+252
-17
@@ -56,7 +56,9 @@ BOOTSTRAP_CM = env("SESSION_BOOTSTRAP_CONFIGMAP", FULLNAME + "-session")
|
||||
ANTHROPIC_SECRET = env("ANTHROPIC_SECRET", FULLNAME + "-anthropic")
|
||||
ANTHROPIC_AUTH_MODE = env("ANTHROPIC_AUTH_MODE", "apiKey")
|
||||
ANTHROPIC_CREDENTIALS_KEY = env("ANTHROPIC_CREDENTIALS_KEY", ".credentials.json")
|
||||
GIT_IDENTITIES = env_json("GIT_IDENTITIES_JSON", {})
|
||||
GIT_IDENTITIES = env_json("GIT_IDENTITIES_JSON", {}) # Helm 管理(読み取り専用)
|
||||
GUI_IDENTITIES_ENABLED = env("GUI_IDENTITIES_ENABLED", "false").lower() == "true"
|
||||
GIT_FULLNAME_PREFIX = env("GIT_FULLNAME_PREFIX", FULLNAME)
|
||||
GIT_ALLOWED_HOSTS = [h.strip().lower() for h in env("GIT_ALLOWED_HOSTS", "").split(",") if h.strip()]
|
||||
GIT_ALLOW_ARBITRARY = env("GIT_ALLOW_ARBITRARY", "false").lower() == "true"
|
||||
GIT_AUTHOR_NAME = env("GIT_DEFAULT_AUTHOR_NAME", "Claude")
|
||||
@@ -103,14 +105,26 @@ LABEL_COMPONENT = "app.kubernetes.io/component"
|
||||
LABEL_SID = "claude-code.cafepieters.com/session-id"
|
||||
ANN_PREFIX = "claude-code.cafepieters.com/"
|
||||
SESSION_SELECTOR = LABEL_COMPONENT + "=session," + LABEL_APP + "=claude-code"
|
||||
LABEL_GIT_IDENT = "claude-code.cafepieters.com/git-identity"
|
||||
LABEL_IDENT_SOURCE = "claude-code.cafepieters.com/identity-source"
|
||||
LABEL_IDENT_TYPE = "claude-code.cafepieters.com/identity-type"
|
||||
|
||||
SID_RE = re.compile(r"^[a-f0-9]{10}$")
|
||||
# 受け入れる repo URL: https://host/... または ssh://git@host/... または git@host:path
|
||||
# identity 名は Secret 名の一部になるため DNS ラベル相当に制限
|
||||
IDENT_NAME_RE = re.compile(r"^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$")
|
||||
# 受け入れる repo URL:
|
||||
# https://host[:port]/path
|
||||
# ssh://[user@]host[:port]/path
|
||||
# [user@]host:path (SCP 形式。Gitea/GitHub の SSH ユーザーは常に "git")
|
||||
_USER = r"(?:[A-Za-z0-9_.\-]+@)?"
|
||||
REPO_RE = re.compile(
|
||||
r"^(?:https://(?P<h1>[A-Za-z0-9.\-]+)(?::\d+)?/[\w./\-~%]+"
|
||||
r"|ssh://git@(?P<h2>[A-Za-z0-9.\-]+)(?::\d+)?/[\w./\-~%]+"
|
||||
r"|git@(?P<h3>[A-Za-z0-9.\-]+):[\w./\-~%]+)$"
|
||||
r"|ssh://" + _USER + r"(?P<h2>[A-Za-z0-9.\-]+)(?::\d+)?/[\w./\-~%]+"
|
||||
r"|" + _USER + r"(?P<h3>[A-Za-z0-9.\-]+):(?!//)[\w./\-~%]+)$"
|
||||
)
|
||||
REPO_HELP = ("URL の形式: https://<host>/<org>/<repo>.git / "
|
||||
"ssh://git@<host>/<org>/<repo>.git / git@<host>:<org>/<repo>.git "
|
||||
"(Gitea・GitHub の SSH ユーザーは常に 'git'。あなたのアカウント名ではありません)")
|
||||
REF_RE = re.compile(r"^[\w./\-]{1,200}$")
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
@@ -166,6 +180,137 @@ def session_log(sid, tail=800):
|
||||
return 400, "bad session id"
|
||||
return k8s_text(core("/pods/claude-sess-" + sid + "/log?container=claude&tailLines=" + str(int(tail))))
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# git identity(Helm 管理 + GUI 登録)
|
||||
# --------------------------------------------------------------------------
|
||||
def gui_identity_secret(name):
|
||||
return GIT_FULLNAME_PREFIX + "-git-" + name
|
||||
|
||||
|
||||
def list_gui_identities():
|
||||
"""GUI から登録された identity(source=gui ラベルの Secret)の一覧。"""
|
||||
if not GUI_IDENTITIES_ENABLED:
|
||||
return []
|
||||
status, payload = k8s("GET", core("/secrets?labelSelector=" +
|
||||
urllib.parse.quote(LABEL_IDENT_SOURCE + "=gui")))
|
||||
if status != 200:
|
||||
log.error("list identity secrets failed: %s", status)
|
||||
return []
|
||||
out = []
|
||||
for sec in payload.get("items", []):
|
||||
lbl = sec.get("metadata", {}).get("labels", {})
|
||||
nm = lbl.get(LABEL_GIT_IDENT)
|
||||
if nm:
|
||||
out.append({"name": nm, "type": lbl.get(LABEL_IDENT_TYPE, "ssh"), "source": "gui"})
|
||||
return out
|
||||
|
||||
|
||||
def list_identities():
|
||||
"""UI 用: Helm 管理 + GUI 登録の identity 一覧。"""
|
||||
out = [{"name": n, "type": (v or {}).get("type", "ssh"), "source": "helm"}
|
||||
for n, v in GIT_IDENTITIES.items()]
|
||||
names = {i["name"] for i in out}
|
||||
for i in list_gui_identities():
|
||||
if i["name"] not in names:
|
||||
out.append(i)
|
||||
return sorted(out, key=lambda x: x["name"])
|
||||
|
||||
|
||||
def resolve_identity(name):
|
||||
"""build_pod 用にマウント情報を返す。無ければ None。"""
|
||||
if not name:
|
||||
return None
|
||||
if name in GIT_IDENTITIES:
|
||||
return GIT_IDENTITIES[name]
|
||||
if not GUI_IDENTITIES_ENABLED:
|
||||
return None
|
||||
status, sec = k8s("GET", core("/secrets/" + gui_identity_secret(name)))
|
||||
if status != 200:
|
||||
return None
|
||||
lbl = sec.get("metadata", {}).get("labels", {})
|
||||
if lbl.get(LABEL_IDENT_SOURCE) != "gui":
|
||||
return None
|
||||
typ = lbl.get(LABEL_IDENT_TYPE, "ssh")
|
||||
data = sec.get("data", {}) or {}
|
||||
if typ == "https-token":
|
||||
user = "x-access-token"
|
||||
if data.get("username"):
|
||||
try:
|
||||
user = base64.b64decode(data["username"]).decode("utf-8").strip() or user
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
return {"type": "https-token", "secret": gui_identity_secret(name),
|
||||
"keyToken": "token", "username": user}
|
||||
return {"type": "ssh", "secret": gui_identity_secret(name),
|
||||
"keyPrivateKey": "id", "keyKnownHosts": "known_hosts"}
|
||||
|
||||
|
||||
def create_gui_identity(body):
|
||||
if not GUI_IDENTITIES_ENABLED:
|
||||
return 403, {"error": "GUI からの identity 登録は無効です(git.guiIdentities.enabled)"}
|
||||
name = str(body.get("name", "")).strip()
|
||||
typ = str(body.get("type", "")).strip()
|
||||
if not IDENT_NAME_RE.match(name):
|
||||
return 400, {"error": "identity 名は英小文字・数字・ハイフン(先頭末尾は英数字、40 文字以内)"}
|
||||
if name in GIT_IDENTITIES:
|
||||
return 409, {"error": "Helm 管理の identity と同名です: " + name}
|
||||
if typ == "ssh":
|
||||
key = str(body.get("privateKey", "")).strip()
|
||||
kh = str(body.get("knownHosts", "")).strip()
|
||||
if "PRIVATE KEY" not in key:
|
||||
return 400, {"error": "privateKey が OpenSSH/PEM 秘密鍵ではありません"}
|
||||
if not kh:
|
||||
return 400, {"error": "knownHosts は必須です(`ssh-keyscan <host>` の出力)"}
|
||||
string_data = {"id": key + ("\n" if not key.endswith("\n") else ""), "known_hosts": kh + "\n"}
|
||||
elif typ == "https-token":
|
||||
token = str(body.get("token", "")).strip()
|
||||
if not token:
|
||||
return 400, {"error": "token は必須です"}
|
||||
string_data = {"username": str(body.get("username", "") or "x-access-token").strip(),
|
||||
"token": token}
|
||||
else:
|
||||
return 400, {"error": "type は ssh か https-token"}
|
||||
|
||||
manifest = {
|
||||
"apiVersion": "v1", "kind": "Secret", "type": "Opaque",
|
||||
"metadata": {
|
||||
"name": gui_identity_secret(name),
|
||||
"namespace": SESSIONS_NS,
|
||||
"labels": {
|
||||
LABEL_APP: "claude-code",
|
||||
LABEL_GIT_IDENT: name,
|
||||
LABEL_IDENT_SOURCE: "gui",
|
||||
LABEL_IDENT_TYPE: typ,
|
||||
},
|
||||
},
|
||||
"stringData": string_data,
|
||||
}
|
||||
# 既存なら置き換え(PUT)、無ければ作成(POST)
|
||||
status, _ = k8s("GET", core("/secrets/" + gui_identity_secret(name)))
|
||||
if status == 200:
|
||||
st, payload = k8s("PUT", core("/secrets/" + gui_identity_secret(name)), manifest)
|
||||
else:
|
||||
st, payload = k8s("POST", core("/secrets"), manifest)
|
||||
if st not in (200, 201):
|
||||
return st, {"error": "Secret 保存に失敗: %s" % payload.get("message", st)}
|
||||
log.info("gui identity %s (%s) saved", name, typ)
|
||||
return 200, {"ok": True, "name": name, "type": typ}
|
||||
|
||||
|
||||
def delete_gui_identity(name):
|
||||
if not GUI_IDENTITIES_ENABLED:
|
||||
return 403, {"error": "無効です"}
|
||||
if not IDENT_NAME_RE.match(name):
|
||||
return 400, {"error": "bad name"}
|
||||
status, sec = k8s("GET", core("/secrets/" + gui_identity_secret(name)))
|
||||
if status != 200:
|
||||
return 404, {"error": "見つかりません"}
|
||||
if sec.get("metadata", {}).get("labels", {}).get(LABEL_IDENT_SOURCE) != "gui":
|
||||
return 403, {"error": "Helm 管理の identity は GUI から削除できません"}
|
||||
k8s("DELETE", core("/secrets/" + gui_identity_secret(name)))
|
||||
log.info("gui identity %s deleted", name)
|
||||
return 200, {"ok": True}
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# セッション操作
|
||||
# --------------------------------------------------------------------------
|
||||
@@ -266,7 +411,7 @@ def build_pod(sid, repo, ref, identity_name, owner):
|
||||
{"name": "bootstrap", "mountPath": "/bootstrap", "readOnly": True},
|
||||
]
|
||||
|
||||
ident = GIT_IDENTITIES.get(identity_name)
|
||||
ident = resolve_identity(identity_name)
|
||||
if ident:
|
||||
env_list.append({"name": "GIT_IDENTITY_TYPE", "value": ident.get("type", "")})
|
||||
if ident.get("type") == "ssh":
|
||||
@@ -411,13 +556,14 @@ def create_session(repo, ref, identity_name, owner):
|
||||
repo = (repo or "").strip()
|
||||
ref = (ref or "main").strip()
|
||||
if not REPO_RE.match(repo):
|
||||
return None, "リポジトリ URL の形式が不正です"
|
||||
return None, "リポジトリ URL の形式が不正です。" + REPO_HELP
|
||||
if not REF_RE.match(ref):
|
||||
return None, "ref の形式が不正です"
|
||||
return None, "ref(ブランチ / タグ)の形式が不正です"
|
||||
host = repo_host(repo)
|
||||
if not GIT_ALLOW_ARBITRARY and host not in GIT_ALLOWED_HOSTS:
|
||||
return None, "許可されていない Git ホストです: " + str(host)
|
||||
if identity_name and identity_name not in GIT_IDENTITIES:
|
||||
return None, ("許可されていない Git ホストです: %s(許可済み: %s)"
|
||||
% (host, ", ".join(GIT_ALLOWED_HOSTS) or "なし"))
|
||||
if identity_name and resolve_identity(identity_name) is None:
|
||||
return None, "未知の git identity: " + identity_name
|
||||
if active_session_count() >= MAX_SESSIONS:
|
||||
return None, "同時セッション数の上限(%d)に達しています" % MAX_SESSIONS
|
||||
@@ -570,13 +716,51 @@ pre#logbox{white-space:pre-wrap;background:#111;color:#eee;padding:.8rem;border-
|
||||
<div id="app" hidden>
|
||||
<form onsubmit="return doCreate(event)">
|
||||
<label for="repo">リポジトリ URL</label>
|
||||
<input id="repo" placeholder="https://github.com/owner/repo.git または git@host:owner/repo.git" required>
|
||||
<input id="repo" placeholder="https://host/org/repo.git または git@host:org/repo.git(SSH ユーザーは常に git)" required>
|
||||
<div class="muted">SSH は <code>git@<host>:<org>/<repo>.git</code>(ユーザー名は自分のアカウントではなく必ず <code>git</code>)。認証は下で選ぶ identity の鍵で行われます。</div>
|
||||
<div class="row">
|
||||
<div style="flex:1"><label for="ref">ブランチ / タグ</label><input id="ref" value="main"></div>
|
||||
<div style="flex:1"><label for="ident">認証 identity</label><select id="ident"></select></div>
|
||||
</div>
|
||||
<button type="submit">セッション作成</button>
|
||||
</form>
|
||||
<div class="card" id="identcard" hidden>
|
||||
<div class="row" style="justify-content:space-between">
|
||||
<strong>認証情報(identity)</strong>
|
||||
<button class="sec" style="width:auto;margin:0" onclick="loadIdents()">更新</button>
|
||||
</div>
|
||||
<table><tbody id="identrows"></tbody></table>
|
||||
<details style="margin-top:.6rem">
|
||||
<summary style="cursor:pointer;font-weight:600">+ identity を追加</summary>
|
||||
<form onsubmit="return addIdent(event)" style="border:0;padding:.4rem 0 0;margin:0">
|
||||
<div class="row">
|
||||
<div style="flex:1"><label for="idn">名前</label>
|
||||
<input id="idn" placeholder="gitea-ssh" pattern="[a-z0-9][a-z0-9-]{0,38}[a-z0-9]" required></div>
|
||||
<div style="flex:1"><label for="idt">種類</label>
|
||||
<select id="idt" onchange="identFields()">
|
||||
<option value="ssh">SSH 秘密鍵</option>
|
||||
<option value="https-token">HTTPS トークン</option>
|
||||
</select></div>
|
||||
</div>
|
||||
<div id="f-ssh">
|
||||
<label for="idkey">SSH 秘密鍵(PEM / OpenSSH)</label>
|
||||
<textarea id="idkey" rows="6" style="width:100%;font-family:monospace;font-size:.8rem"
|
||||
placeholder="-----BEGIN OPENSSH PRIVATE KEY----- ..."></textarea>
|
||||
<label for="idkh">known_hosts(必須)</label>
|
||||
<textarea id="idkh" rows="2" style="width:100%;font-family:monospace;font-size:.8rem"
|
||||
placeholder="ssh-keyscan <host> の出力を貼り付け"></textarea>
|
||||
</div>
|
||||
<div id="f-tok" hidden>
|
||||
<label for="idu">ユーザー名</label>
|
||||
<input id="idu" value="x-access-token">
|
||||
<label for="idtk">トークン(PAT)</label>
|
||||
<input id="idtk" type="password" placeholder="Gitea / GitHub の Personal Access Token">
|
||||
</div>
|
||||
<button type="submit">保存</button>
|
||||
<div class="muted">秘密鍵はブラウザ→サーバーへ送信され Secret として保存されます。経路の TLS を有効にしてください。</div>
|
||||
</form>
|
||||
</details>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="row" style="justify-content:space-between">
|
||||
<strong>セッション一覧</strong>
|
||||
@@ -598,17 +782,55 @@ pre#logbox{white-space:pre-wrap;background:#111;color:#eee;padding:.8rem;border-
|
||||
const $=s=>document.querySelector(s);
|
||||
let identities=[];
|
||||
function api(path,opts){opts=opts||{};opts.credentials='same-origin';return fetch(path,opts);}
|
||||
let guiIdents=false;
|
||||
async function boot(){
|
||||
const r=await api('/api/config');
|
||||
if(r.status===401){$('#login').hidden=false;return;}
|
||||
const c=await r.json();
|
||||
identities=c.identities||[];
|
||||
guiIdents=!!c.guiIdentities;
|
||||
$('#idle').textContent=c.idleTimeoutSeconds;
|
||||
const sel=$('#ident');
|
||||
sel.innerHTML='<option value="">(認証なし / 公開リポジトリ)</option>'+identities.map(i=>`<option>${i}</option>`).join('');
|
||||
$('#app').hidden=false;
|
||||
$('#identcard').hidden=!guiIdents;
|
||||
applyIdents(c.identities||[]);
|
||||
if(guiIdents) loadIdents();
|
||||
refresh();
|
||||
}
|
||||
function applyIdents(list){
|
||||
identities=list;
|
||||
$('#ident').innerHTML='<option value="">(認証なし / 公開リポジトリ)</option>'+
|
||||
list.map(i=>`<option>${esc(i.name)}</option>`).join('');
|
||||
}
|
||||
function identFields(){
|
||||
const ssh=$('#idt').value==='ssh';
|
||||
$('#f-ssh').hidden=!ssh;$('#f-tok').hidden=ssh;
|
||||
}
|
||||
async function loadIdents(){
|
||||
const r=await api('/api/identities');if(!r.ok)return;
|
||||
const list=(await r.json()).identities||[];
|
||||
applyIdents(list);
|
||||
$('#identrows').innerHTML=list.map(i=>`<tr>
|
||||
<td><code>${esc(i.name)}</code></td><td class="muted">${i.type}</td>
|
||||
<td class="muted">${i.source}</td>
|
||||
<td style="text-align:right">${i.source==='gui'
|
||||
?`<button class="sec" style="width:auto;margin:0" onclick="delIdent('${esc(i.name)}')">削除</button>`
|
||||
:'<span class="muted">Helm 管理</span>'}</td></tr>`).join('')
|
||||
||'<tr><td class="muted">未登録</td></tr>';
|
||||
}
|
||||
async function addIdent(e){e.preventDefault();
|
||||
const t=$('#idt').value;
|
||||
const body={name:$('#idn').value.trim(),type:t};
|
||||
if(t==='ssh'){body.privateKey=$('#idkey').value;body.knownHosts=$('#idkh').value;}
|
||||
else{body.username=$('#idu').value.trim();body.token=$('#idtk').value;}
|
||||
const r=await api('/api/identities',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
|
||||
const j=await r.json();
|
||||
if(!r.ok){alert(j.error||'保存に失敗しました');return false;}
|
||||
$('#idn').value='';$('#idkey').value='';$('#idkh').value='';$('#idtk').value='';
|
||||
loadIdents();return false;
|
||||
}
|
||||
async function delIdent(name){if(!confirm('identity '+name+' を削除しますか?'))return;
|
||||
const r=await api('/api/identities/'+encodeURIComponent(name),{method:'DELETE'});
|
||||
if(!r.ok){const j=await r.json();alert(j.error||'削除に失敗しました');}
|
||||
loadIdents();}
|
||||
async function doLogin(e){e.preventDefault();
|
||||
const r=await api('/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token:$('#tk').value})});
|
||||
if(r.ok){location.reload();}else{alert('トークンが違います');}
|
||||
@@ -713,12 +935,17 @@ class Handler(BaseHTTPRequestHandler):
|
||||
if not token_ok(self):
|
||||
return self._json(401, {"error": "unauthorized"})
|
||||
return self._json(200, {
|
||||
"identities": sorted(GIT_IDENTITIES.keys()),
|
||||
"identities": list_identities(),
|
||||
"guiIdentities": GUI_IDENTITIES_ENABLED,
|
||||
"idleTimeoutSeconds": IDLE_TIMEOUT,
|
||||
"maxLifetimeSeconds": MAX_LIFETIME,
|
||||
"maxSessions": MAX_SESSIONS,
|
||||
"authMode": AUTH_MODE,
|
||||
})
|
||||
if path == "/api/identities":
|
||||
if not token_ok(self):
|
||||
return self._json(401, {"error": "unauthorized"})
|
||||
return self._json(200, {"identities": list_identities()})
|
||||
if path == "/api/sessions":
|
||||
if not token_ok(self):
|
||||
return self._json(401, {"error": "unauthorized"})
|
||||
@@ -752,6 +979,9 @@ class Handler(BaseHTTPRequestHandler):
|
||||
if err:
|
||||
return self._json(400, {"error": err})
|
||||
return self._json(201, {"id": sid, "url": "/s/" + sid + "/"})
|
||||
if path == "/api/identities":
|
||||
code, payload = create_gui_identity(self._body())
|
||||
return self._json(code, payload)
|
||||
m = re.match(r"^/api/sessions/([a-f0-9]{10})/touch$", path)
|
||||
if m:
|
||||
touch_session(m.group(1), min_interval=0)
|
||||
@@ -761,11 +991,16 @@ class Handler(BaseHTTPRequestHandler):
|
||||
def do_DELETE(self):
|
||||
if not token_ok(self):
|
||||
return self._json(401, {"error": "unauthorized"})
|
||||
m = re.match(r"^/api/sessions/([a-f0-9]{10})$", urllib.parse.urlparse(self.path).path)
|
||||
if not m:
|
||||
return self._send(404, b'{"error":"not found"}')
|
||||
path = urllib.parse.urlparse(self.path).path
|
||||
m = re.match(r"^/api/sessions/([a-f0-9]{10})$", path)
|
||||
if m:
|
||||
delete_session(m.group(1))
|
||||
return self._json(200, {"ok": True})
|
||||
m = re.match(r"^/api/identities/([a-z0-9][a-z0-9-]{0,39})$", path)
|
||||
if m:
|
||||
code, payload = delete_gui_identity(m.group(1))
|
||||
return self._json(code, payload)
|
||||
return self._send(404, b'{"error":"not found"}')
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -137,6 +137,10 @@ spec:
|
||||
value: {{ .Values.anthropic.existingSecretKeyCredentials | default ".credentials.json" | quote }}
|
||||
- name: GIT_IDENTITIES_JSON
|
||||
value: {{ $idents | toJson | quote }}
|
||||
- name: GUI_IDENTITIES_ENABLED
|
||||
value: {{ .Values.git.guiIdentities.enabled | quote }}
|
||||
- name: GIT_FULLNAME_PREFIX
|
||||
value: {{ include "claude-code.fullname" . | quote }}
|
||||
- name: GIT_ALLOWED_HOSTS
|
||||
value: {{ join "," .Values.git.allowedHosts | quote }}
|
||||
- name: GIT_ALLOW_ARBITRARY
|
||||
|
||||
@@ -22,6 +22,14 @@ rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["events"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
{{- if .Values.git.guiIdentities.enabled }}
|
||||
# GUI からの git identity 登録(Secret の作成/更新/削除)。
|
||||
# list は同 NS の他 Secret も対象になりうるが、manager コードは
|
||||
# ラベル claude-code.cafepieters.com/identity-source=gui のものだけを操作する。
|
||||
- apiGroups: [""]
|
||||
resources: ["secrets"]
|
||||
verbs: ["get", "list", "create", "update", "patch", "delete"]
|
||||
{{- end }}
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
|
||||
@@ -12,6 +12,8 @@ metadata:
|
||||
labels:
|
||||
{{- include "claude-code.labels" $ctx | nindent 4 }}
|
||||
claude-code.cafepieters.com/git-identity: {{ $name | quote }}
|
||||
claude-code.cafepieters.com/identity-source: helm
|
||||
claude-code.cafepieters.com/identity-type: {{ $id.type }}
|
||||
type: Opaque
|
||||
stringData:
|
||||
{{- if eq $id.type "ssh" }}
|
||||
|
||||
@@ -141,6 +141,11 @@
|
||||
"allowArbitraryRepos": { "type": "boolean" },
|
||||
"defaultAuthorName": { "type": "string" },
|
||||
"defaultAuthorEmail": { "type": "string" },
|
||||
"guiIdentities": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": { "enabled": { "type": "boolean" } }
|
||||
},
|
||||
"identities": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
|
||||
+16
-3
@@ -30,7 +30,7 @@ image:
|
||||
gateway:
|
||||
registry: docker.io
|
||||
repository: nginxinc/nginx-unprivileged
|
||||
tag: "1.31.5-alpine"
|
||||
tag: "1.31.6-alpine"
|
||||
pullPolicy: IfNotPresent
|
||||
# 制御プレーン(Python 標準ライブラリのみ・追加 pip 不要)
|
||||
manager:
|
||||
@@ -50,7 +50,7 @@ image:
|
||||
session:
|
||||
registry: git.cafepieters.com
|
||||
repository: helmchart/claude-code/session
|
||||
tag: "2.1.263"
|
||||
tag: "2.1.278"
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
imagePullSecrets: []
|
||||
@@ -216,8 +216,21 @@ git:
|
||||
defaultAuthorName: "Claude"
|
||||
defaultAuthorEmail: "claude@cafepieters.com"
|
||||
|
||||
# 認証アイデンティティ。セッション作成時に名前で選択する。
|
||||
# GUI(Web UI)からの認証情報 identity 登録を許可する。
|
||||
# 有効にすると、UI の「認証情報」カードから SSH 秘密鍵 + known_hosts / HTTPS トークンを
|
||||
# 登録・削除できる。登録内容はセッション NS の Secret として保存される。
|
||||
# トレードオフ:
|
||||
# - manager にセッション NS の secrets への get/list/create/update/delete 権限が付く
|
||||
# (同 NS には Claude 認証情報 Secret も存在する点に留意)。
|
||||
# - 秘密鍵はブラウザ→gateway→manager をリクエストボディで通る。経路の TLS
|
||||
# (Ingress の tls / LB の TLS 終端)を必ず有効にすること。
|
||||
# 下の git.identities(Helm 管理)は常に読み取り専用で UI に表示される。
|
||||
guiIdentities:
|
||||
enabled: true
|
||||
|
||||
# 認証アイデンティティ(Helm 管理)。セッション作成時に名前で選択する。
|
||||
# GitHub / Gitea / GitLab など SSH または HTTPS トークンで認証する任意のホストに対応。
|
||||
# GUI から登録したものと合わせて使える(同名は不可)。
|
||||
identities: {}
|
||||
# gitea-ssh:
|
||||
# type: ssh # ssh | https-token
|
||||
|
||||
Reference in New Issue
Block a user