Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/user/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,17 @@ devbase はホストマシンの認証情報を自動収集し、コンテナ内
| `SLACK_CHANNEL_ID` | チャンネル ID |
| `SLACK_USER_MENTION` | ユーザーメンション |

#### host -- ホスト接続情報 (SSH)

コンテナからホストへ SSH してホスト側 GUI アプリ(例: Chrome をリモートデバッグモードで起動)を起動するワークフロー向けの設定です。`devbase env init` はホスト上で実行されるため、ホストのログインユーザー名を自動取得して既定値として提示します。

| キー | 説明 |
|------|------|
| `HOST_SSH_USER` | コンテナ→ホスト SSH 時のホストログインユーザー名(既定: `getpass.getuser()` で自動取得) |
| `HOST_SSH_HOST` | SSH 先ホスト名(既定: `host.docker.internal`、WSL2/Windows では上書き可) |

ユーザー名のみで秘密情報ではありません。SSH 鍵やリモートログインの有効化はホスト側でユーザーが別途設定する前提です。`devbase env sync` 実行時には、未設定のキーのみ既定値で補完されます(既存値は上書きしません)。

## ソースファイル変更検出

devbase はソースファイル(`~/.aws/config` 等)のハッシュを `.env.sources.yml` で管理しています。
Expand Down
195 changes: 195 additions & 0 deletions issues/PLAN48_host-ssh-user-env.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
# PLAN48: `devbase env init` で `HOST_SSH_USER` を自動設定する

## 関連リンク

- 元 issue: [#48](https://github.com/devbasex/devbase/issues/48) `feat: devbase env init で HOST_SSH_USER を自動設定する`
- 利用側 (参照のみ・本リポジトリ対象外): ai-plugins `plugins/ndf/skills/playwright-browser-connect/`
(`scripts/start-host-chrome.sh` がコンテナ→ホスト SSH のために `HOST_SSH_USER` を要求する)

## 概要

`devbase env init` の対話セットアップに **ホスト接続情報 (SSH)** コレクタを追加し、
ホスト (mac/Linux/WSL) のログインユーザー名を `HOST_SSH_USER` として `.env` に自動設定する。
併せて SSH 先ホスト名 `HOST_SSH_HOST`(既定 `host.docker.internal`)も収集する。

これにより、コンテナからホストへ SSH してホスト側 GUI アプリ(例: Chrome をリモート
デバッグモードで起動)を起動するワークフローを、`HOST_SSH_USER=<名>` の手動指定なしで
利用できるようにする。

`devbase env init` は **ホスト上で実行される CLI** であり、ホストのユーザー名を確実に
取得できる立場にある(`getpass.getuser()`)。

## 設計判断(確定事項)

| 論点 | 決定 | 理由 |
|---|---|---|
| 収集キー | `HOST_SSH_USER` + `HOST_SSH_HOST` 両方 | WSL2/Windows ではホストユーザーと SSH 先が一致しないケースがあり、`HOST_SSH_HOST` の上書き余地を残す(issue 補足) |
| `devbase env sync` 対応 | あり(欠落キーの補完) | 既存ユーザーの `.env` への後付け backfill。ただし手動上書きは尊重して上書きしない |
| 進め方 | 単一 PR | 変更 ~4 ファイル・~100 行・依存タスクなし。release ブランチ不要 |

## アーキテクチャ整合

既存のコレクタ機構に倣う:

- `lib/devbase/env/collector.py` の `CollectorRegistry` は `env/collectors/*.py` を走査し、
モジュール直下の **`COLLECTOR` 定数**(大文字)を登録する。issue 例の `collector =` は
実装規約と異なるため `COLLECTOR =` で定義する。
- 既存 `git.py` / `slack.py` と同じ `Collector(name, display_name, collect_fn)` インターフェース。
- 対話入力は `devbase.env.store.safe_input`(EOF 安全。非対話/CI では default を返す)を使う。

```mermaid
flowchart LR
init["devbase env init"] --> reg["CollectorRegistry.discover()"]
reg --> host["collectors/host.py<br/>COLLECTOR"]
host --> collect["collect_host_info(env_file)"]
collect --> envfile[".env<br/>HOST_SSH_USER / HOST_SSH_HOST"]
sync["devbase env sync"] --> backfill["_sync_host()<br/>欠落キーのみ補完"]
backfill --> envfile
```

## 変更ファイル

| ファイル | 種別 | 内容 |
|---|---|---|
| `lib/devbase/env/keys.py` | 変更 | `HOST_SSH_USER` / `HOST_SSH_HOST` 定数を追加 |
| `lib/devbase/env/collectors/host.py` | 新規 | `host` コレクタ(`collect_host_info` + `COLLECTOR`) |
| `lib/devbase/commands/env.py` | 変更 | `cmd_env_sync` に `_sync_host()` を追加 |
| `docs/user/environment-variables.md` | 変更 | `#### host` 節とコレクタ一覧へ追記 |
| `tests/env/test_collector_host.py` | 新規 | コレクタ + sync の単体テスト |

## 実装詳細

### 1. `keys.py`

```python
# --- Host (コンテナ→ホスト SSH 接続) ---
HOST_SSH_USER = "HOST_SSH_USER"
HOST_SSH_HOST = "HOST_SSH_HOST" # 任意。default: host.docker.internal
```

### 2. `collectors/host.py`(新規)

```python
"""ホスト接続情報 (SSH) コレクター"""

import getpass

from devbase.log import get_logger
from devbase.env import keys
from devbase.env.store import EnvFile, safe_input
from devbase.env.collector import Collector

logger = get_logger(__name__)

DEFAULT_HOST_SSH_HOST = "host.docker.internal"


def _default_host_user() -> str:
"""ホストのログインユーザー名。HOME/USER/LOGNAME 欠落時も例外を出さず "" を返す。"""
try:
return getpass.getuser()
except Exception:
return ""


def collect_host_info(env_file: EnvFile) -> None:
"""ホスト接続情報 (SSH) を対話的に収集する"""
print("\n=== ホスト接続情報 (SSH) ===")

default_user = env_file.get(keys.HOST_SSH_USER) or _default_host_user()
user = safe_input(f"{keys.HOST_SSH_USER} [{default_user}]: ", default_user)
if user:
env_file.set(keys.HOST_SSH_USER, user)
else:
logger.info("%s: 既定値が取得できずスキップ", keys.HOST_SSH_USER)

default_host = env_file.get(keys.HOST_SSH_HOST) or DEFAULT_HOST_SSH_HOST
host = safe_input(f"{keys.HOST_SSH_HOST} [{default_host}]: ", default_host)
if host:
env_file.set(keys.HOST_SSH_HOST, host)


COLLECTOR = Collector(
name="host",
display_name="ホスト接続情報 (SSH)",
collect_fn=collect_host_info,
)
```

非対話 (EOF) 時: `safe_input` が default を返すため `HOST_SSH_USER=getpass.getuser()`・
`HOST_SSH_HOST=host.docker.internal` が設定される(受け入れ条件「非対話/CI でも既定値で設定」)。
`getpass.getuser()` が空を返す環境では `HOST_SSH_USER` は安全にスキップされる。

### 3. `cmd_env_sync` への `_sync_host()` 追加

ホスト情報はソースファイルを持たないため hash 比較は使わず、**欠落キーのみ既定値で補完**する
(既存値=WSL2 等での手動上書きは尊重して上書きしない)。既存ユーザーの `.env` への後付け
backfill として機能する。

```python
def _sync_host(env_file):
"""ホスト接続情報の同期。欠落キーを既定値で補完する。更新件数を返す。"""
from devbase.env.collectors.host import _default_host_user, DEFAULT_HOST_SSH_HOST
updated = 0
if not env_file.get(keys.HOST_SSH_USER):
user = _default_host_user()
if user:
env_file.set(keys.HOST_SSH_USER, user)
logger.info("HOST_SSH_USER: %s を設定", user)
updated += 1
if not env_file.get(keys.HOST_SSH_HOST):
env_file.set(keys.HOST_SSH_HOST, DEFAULT_HOST_SSH_HOST)
logger.info("HOST_SSH_HOST: %s を設定", DEFAULT_HOST_SSH_HOST)
updated += 1
return updated
```

`cmd_env_sync` 内で `updated += _sync_host(env_file)` を呼ぶ。`updated > 0` なら既存ロジック
どおり `env_file.save()` まで到達する。

### 4. ドキュメント (`docs/user/environment-variables.md`)

「コレクター一覧」に `host` を追加し、slack 節の後に以下を追記:

```markdown
#### host -- ホスト接続情報 (SSH)

| キー | 説明 |
|------|------|
| `HOST_SSH_USER` | コンテナ→ホスト SSH 時のホストログインユーザー名(既定: `getpass.getuser()`) |
| `HOST_SSH_HOST` | SSH 先ホスト名(既定: `host.docker.internal`、WSL2/Windows では上書き可) |

ユーザー名のみで秘密情報ではない。SSH 鍵・リモートログイン有効化はホスト側で別途設定する前提。
```

## テスト計画 (`tests/env/test_collector_host.py`)

`input` / `getpass.getuser` を monkeypatch した単体テスト(本コレクタは questionary を
使わない素の `input`/`safe_input` 経路のため、real TTY は不要):

1. **既定値設定**: `getuser`→"alice"、入力 EOF → `HOST_SSH_USER=alice` / `HOST_SSH_HOST=host.docker.internal`
2. **上書き**: 入力で "bob" / "192.168.1.10" → その値で設定される
3. **getuser 例外**: `getuser` が例外 → `HOST_SSH_USER` 未設定・`HOST_SSH_HOST` は既定で設定
4. **既存値優先**: env に既存 `HOST_SSH_USER=carol` → default 提示が carol
5. **`_sync_host` backfill**: 欠落時に補完・既存値は上書きしない・更新件数が正しい
6. **レジストリ登録**: `CollectorRegistry.discover()` 後に name=="host" が含まれる

既存テスト一式が回帰しないこと(`pytest tests/`)も確認する。

## 受け入れ条件(issue より)

- [ ] `devbase env init` 実行時に `HOST_SSH_USER` の既定値(ホストのユーザー名)が提示され、確認・変更できる
- [ ] `.env` に `HOST_SSH_USER` が書き出され、コンテナ内の環境変数として参照できる
- [ ] 非対話/CI でも既定値(`getpass.getuser()`)で設定される、もしくは安全にスキップされる
- [ ] `HOST_SSH_HOST`(既定 `host.docker.internal`)を収集し、上書きできる
- [ ] `devbase env sync` で欠落キーを既定値で補完する(既存値は尊重)
- [ ] ドキュメント(`docs/user/environment-variables.md`)に `HOST_SSH_USER` / `HOST_SSH_HOST` を追記

## PR 計画

| PR | branch | base | 概要 |
|---|---|---|---|
| 単一 | `feature/PLAN48-host-ssh-user` | `main` | 上記 4 ファイル変更 + テスト。`/ndf:cross-review` でセルフレビュー後 merge |

release ブランチは作成しない(単一 PR・低結合)。`/ndf:implementation-plan` 確認 →
実装 → `/ndf:pr` → `/ndf:cross-review` の通常フローで進める。
26 changes: 26 additions & 0 deletions lib/devbase/commands/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ def _encode_git():
# GCP(プロファイル管理があるため個別処理)
updated += _sync_gcp(sources, env_file)

# Host 接続情報(ソースファイルを持たないため hash 比較せず欠落キーを補完)
updated += _sync_host(env_file)

if updated > 0:
env_file.save()
_update_source_metadata(devbase_root, env_file)
Expand All @@ -129,6 +132,29 @@ def _encode_git():
return 0


def _sync_host(env_file):
"""ホスト接続情報の同期。更新件数を返す。

ホスト情報はソースファイルを持たないため hash 比較は使わず、**欠落キーのみ既定値で
補完**する。既存値 (WSL2 等での手動上書き) は尊重して上書きしない。これにより本機能
導入前の ``.env`` への後付け backfill として機能する。
"""
from devbase.env.collectors.host import _default_host_user, DEFAULT_HOST_SSH_HOST

updated = 0
if not env_file.get(keys.HOST_SSH_USER):
user = _default_host_user()
if user:
env_file.set(keys.HOST_SSH_USER, user)
logger.info("%s: %s を設定", keys.HOST_SSH_USER, user)
updated += 1
if not env_file.get(keys.HOST_SSH_HOST):
env_file.set(keys.HOST_SSH_HOST, DEFAULT_HOST_SSH_HOST)
logger.info("%s: %s を設定", keys.HOST_SSH_HOST, DEFAULT_HOST_SSH_HOST)
updated += 1
return updated


def _sync_source(sources, env_file, name, label, encode_fn):
"""AWS/Gitなどの単一ソース同期の共通処理。更新件数(0 or 1)を返す。"""
source = sources.get_source(name)
Expand Down
56 changes: 56 additions & 0 deletions lib/devbase/env/collectors/host.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""ホスト接続情報 (SSH) コレクター

コンテナからホストへ SSH してホスト側 GUI アプリ (例: Chrome をリモートデバッグ
モードで起動) を起動するワークフロー向けに、ホストのログインユーザー名 / SSH 先
ホスト名を収集する。``devbase env init`` はホスト上で実行されるため、ホストの
ユーザー名を ``getpass.getuser()`` で確実に取得できる。
"""

import getpass

from devbase.log import get_logger
from devbase.env import keys
from devbase.env.store import EnvFile, safe_input
from devbase.env.collector import Collector

logger = get_logger(__name__)

DEFAULT_HOST_SSH_HOST = "host.docker.internal"


def _default_host_user() -> str:
"""ホストのログインユーザー名を返す。

``getpass.getuser()`` は HOME/USER/LOGNAME 等が全て無い環境で例外を投げうるため、
その場合は空文字を返して呼び出し側で安全にスキップできるようにする。
"""
try:
return getpass.getuser()
except Exception:
return ""


def collect_host_info(env_file: EnvFile) -> None:
"""ホスト接続情報 (SSH) を対話的に収集する"""
print("\n=== ホスト接続情報 (SSH) ===")

# HOST_SSH_USER: 既存値 > getpass.getuser() を既定として提示し、上書き可能にする
default_user = env_file.get(keys.HOST_SSH_USER) or _default_host_user()
user = safe_input(f"{keys.HOST_SSH_USER} [{default_user}]: ", default_user)
if user:
env_file.set(keys.HOST_SSH_USER, user)
else:
logger.info("%s: 既定値が取得できずスキップ", keys.HOST_SSH_USER)

# HOST_SSH_HOST: 任意。既定 host.docker.internal (WSL2/Windows では上書き可)
default_host = env_file.get(keys.HOST_SSH_HOST) or DEFAULT_HOST_SSH_HOST
host = safe_input(f"{keys.HOST_SSH_HOST} [{default_host}]: ", default_host)
if host:
env_file.set(keys.HOST_SSH_HOST, host)


COLLECTOR = Collector(
name="host",
display_name="ホスト接続情報 (SSH)",
collect_fn=collect_host_info,
)
4 changes: 4 additions & 0 deletions lib/devbase/env/keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,7 @@ def gcp_credentials_key(profile: str) -> str:
# --- Slack ---
SLACK_KEYS = ("SLACK_BOT_TOKEN", "SLACK_TEAM_ID",
"SLACK_CHANNEL_ID", "SLACK_USER_MENTION")

# --- Host (コンテナ→ホスト SSH 接続) ---
HOST_SSH_USER = "HOST_SSH_USER"
HOST_SSH_HOST = "HOST_SSH_HOST" # 任意。default: host.docker.internal
Loading
Loading