diff --git a/docs/specifications/remote-docker-context.md b/docs/specifications/remote-docker-context.md index 722dadc4..d0f6a1e1 100644 --- a/docs/specifications/remote-docker-context.md +++ b/docs/specifications/remote-docker-context.md @@ -119,7 +119,11 @@ root 所有か rootless Docker の可能性を警告して続行する。控え (ssh 先のときだけ)docker が実際に使う context(環境変数を外さない `docker context show`)」の 順で決める。解決した context があればローカル端末でもフラット URI に付ける。Remote-SSH 統合 端末でネスト URI と `settings.context` の両方が付くときは、手元の VS Code に同名の context が -あれば直接 attach できるフラット URI を info で提示する。 +あれば直接 attach できるフラット URI を info で提示し、`DEVBASE_EDITOR_SSH_HOST=`(空)で +恒久的にそのフラット URI へ切り替えられることも示す。空文字は自動検出のオプトアウトで、 +ネストを付けず `settings.context` だけを残す。明示と devbase の解決結果は ssh の有無に +よらず付けるが、`docker context show` の推測は ssh 先のときに限るため、推測だけが元の +ときは空文字で `settings.context` も消える。その場合は恒久化の案内を出さない。 ```mermaid sequenceDiagram diff --git a/docs/user/environment-variables.md b/docs/user/environment-variables.md index 35be85d9..378265cd 100644 --- a/docs/user/environment-variables.md +++ b/docs/user/environment-variables.md @@ -277,7 +277,7 @@ DEVBASE_ACCOUNT_GROUP=kkg | `DEVBASE_EDITOR` | 起動コマンド(既定: `code`)。`cursor` / `code-insiders` 等も可 | | `DEVBASE_WORKSPACE` | 開く `*.code-workspace` ファイルの**コンテナ内絶対パス**を明示指定する(例 `/home/ubuntu/share/work/uttarov2-doc.workspace`)。**効くのはリポジトリ 1 件の構成だけ**です。2 件以上の構成では `devbase up` が自動生成した `/work/<プロジェクト名>.code-workspace` を直接開くため、この env を設定しても上書きできません。`~/share`(= 全コンテナ共有ボリューム `/persistent/ai/share` への symlink)配下に置けば全コンテナで共用可 | | `DEVBASE_OPEN_INDEX` | scale 時に開く dev インスタンス番号(既定: `1`) | -| `DEVBASE_EDITOR_SSH_HOST` | Remote-SSH 跨ホスト構成での ssh-remote ホスト名(例 `mac2`)。**通常は `~/.vscode-server` から自動検出**され不要。検出が外れる場合のみ明示。下記「リモート Docker」参照 | +| `DEVBASE_EDITOR_SSH_HOST` | Remote-SSH 跨ホスト構成での ssh-remote ホスト名(例 `mac2`)。**通常は `~/.vscode-server` から自動検出**され不要。検出が外れる場合のみ明示。**空文字(`DEVBASE_EDITOR_SSH_HOST=`)はネストのオプトアウト**で、フラット URI を強制する。下記「リモート Docker」参照 | | `DEVBASE_EDITOR_DOCKER_CONTEXT` | attach に使う docker context を手で決めたいときだけ明示する。未設定なら devbase が解決した context(`--context` / `DEVBASE_DOCKER_CONTEXT` / `project.local.yml`)、それも無ければ跨ホスト時にホストの `docker context show` | | `DEVBASE_DOCKER_CONTEXT` | `devbase up/down/ps/logs/login/scale/build/rebuild` が向ける docker context。`project.local.yml` の `docker.context` より優先し、CLI `--context` に負ける。グローバル `.env` に書くと全プロジェクトが同じホストへ向くため、通常は `project.local.yml` に書く。下記「リモート Docker」参照 | | `DEVBASE_WINDOW_TITLE` | attach 先 VS Code の `window.title` テンプレート。`{container}` が実コンテナ名(例 `nyle-dx-dev-1`)に置換される。既定は `{container}${separator}${dirty}${activeEditorShort}`。`0` / `false` / `off` / 空文字で無効化。下記「ウィンドウタイトル」参照 | @@ -404,7 +404,7 @@ gid が変わったらファイルを消すか `docker.gid` を書いてくだ | ローカル端末(Mac / Linux / WSL) | 同じマシン(従来) | フラット URI | | ローカル端末 | リモート context | フラット URI + `settings.context=`。手元の Dev Containers 拡張がその context 経由で attach する | | Remote-SSH 統合ターミナル(Windows VS Code → Mac) | Mac | ネスト URI `…@ssh-remote+`(下記) | -| Remote-SSH 統合ターミナル | リモート context(WSL / EC2 など) | ネスト URI + `settings.context=`。Mac の Dev Containers が context 経由で attach する。あわせて、手元の VS Code に同名の context があれば直接 attach できるフラット URI も表示する(Windows → Mac → WSL(Windows) の一周を避けたいとき) | +| Remote-SSH 統合ターミナル | リモート context(WSL / EC2 など) | ネスト URI + `settings.context=`。Mac の Dev Containers が context 経由で attach する。あわせて、手元の VS Code に同名の context があれば直接 attach できるフラット URI も表示する(Windows → Mac → WSL(Windows) の一周を避けたいとき)。毎回そのフラット URI で開きたければ `DEVBASE_EDITOR_SSH_HOST=`(空)を書く(下記「手元の context で直接開く」) | `settings.context` は「`DEVBASE_EDITOR_DOCKER_CONTEXT` の明示 → devbase が解決した context → (ssh 先のときだけ)`docker context show`」の順で決まります。 @@ -430,6 +430,17 @@ DEVBASE_EDITOR_SSH_HOST=mac2 > 同一ホスト構成(手元 Mac/Linux で直接、または ssh 先の Docker にコンテナが無い場合)では ssh-remote ホストは付かず、従来どおりフラット URI で開きます。 +##### 手元の context で直接開く(ネストのオプトアウト) + +コンテナが ssh 先とも別のホスト(例 WSL2)にあり、**手元の VS Code 側にも同名の docker context がある**なら、ssh 先を経由するネスト URI ではなく、手元の Dev Containers 拡張にその context で直接 attach させられます。`DEVBASE_EDITOR_SSH_HOST` を**空文字で明示**すると自動検出を行わず、`settings.context` 付きのフラット URI で `code` を起動します: + +```sh +# projects//env(その context を使うプロジェクトだけに効かせる) +DEVBASE_EDITOR_SSH_HOST= +``` + +前提は 2 つです。1 つは、`code` の実行先(手元の VS Code が docker CLI を呼ぶ側)で `docker --context ps` がコンテナを返すこと。もう 1 つは、`settings.context` が **devbase の解決した context(`project.local.yml` / `--context` / `DEVBASE_DOCKER_CONTEXT`)か `DEVBASE_EDITOR_DOCKER_CONTEXT` の明示**から来ていることです。ssh 先の `docker context show` の推測だけに頼っている構成では、空文字にすると推測も行われず `settings.context` が付かない(手元の既定 daemon へ繋ぎに行く)ため、`devbase up` はこの案内を出しません。Windows の VS Code で `dev.containers.executeInWSL` が `true` なら WSL 内の CLI が使われるので、WSL 内に `docker context create --docker host=unix:///var/run/docker.sock` で同名 context を作れば足ります。行を消せば自動検出(ネスト URI)に戻ります。 + ##### 制約 - `docker context use` で**現在の context 自体**をリモートへ向けた状態は、これまでどおりの diff --git a/lib/devbase/editor/opener.py b/lib/devbase/editor/opener.py index 70d02406..78f5af63 100644 --- a/lib/devbase/editor/opener.py +++ b/lib/devbase/editor/opener.py @@ -669,13 +669,22 @@ def _build_open_uri(ctx: EditorContext, env, container: str, workdir: str, ネスト URI (ssh_host + docker_context) のときは、手元 VS Code に同名 context が あれば ssh 先を経由せず直接 attach できるフラット URI を info ログで提示する。 + あわせて、``DEVBASE_EDITOR_SSH_HOST=`` (空) で恒久的にフラット URI へ切り替えられる + ことも示す (案内だけでは毎回手で貼ることになる)。この案内は context が明示か devbase の + 解決結果から来ているときだけ出す。``docker context show`` の推測だけが元のときは、 + ssh_host を外すと推測も行われず ``settings.context`` が消えるため出さない。 戻り値は ``(uri, uri_flag)``。 """ ssh_host = (resolve_editor_ssh_host(env, auto_detect=ctx.in_vscode) if ctx.is_ssh else None) - if ssh_host or docker_context: - docker_context = resolve_docker_context(env, default=docker_context) + # 明示 (DEVBASE_EDITOR_DOCKER_CONTEXT) と devbase の解決結果は ssh の有無によらず + # 付ける。`docker context show` の推測だけは ssh 先のときに限る (ローカル端末で + # 毎回 docker を叩かない)。 + explicit_context = env.get("DEVBASE_EDITOR_DOCKER_CONTEXT") + resolved_context = docker_context + if ssh_host or resolved_context or explicit_context is not None: + docker_context = resolve_docker_context(env, default=resolved_context) else: docker_context = None # DEVBASE_WORKSPACE があれば *.code-workspace をワークスペースとして開く。VS Code は @@ -694,6 +703,14 @@ def _build_open_uri(ctx: EditorContext, env, container: str, workdir: str, "次で直接 attach できます:", docker_context) logger.info(" %s %s '%s'", " ".join(shlex.quote(c) for c in display), uri_flag, flat) + # 空文字の明示はネストのオプトアウト (resolve_editor_ssh_host)。毎回手で貼る + # 代わりに恒久化する方法を、その場で示す。ただし context が `docker context + # show` の推測だけから来ている場合は、ssh_host を外すと推測も行われず + # settings.context が消える (別の daemon へ繋ぎに行く) ため案内しない。 + if resolved_context or explicit_context is not None: + logger.info( + " env に DEVBASE_EDITOR_SSH_HOST= (空) を書くと、次回からこのフラット URI で" + "直接開きます") return uri, uri_flag diff --git a/tests/editor/test_opener.py b/tests/editor/test_opener.py index b7cd0e6f..9b0bfe18 100644 --- a/tests/editor/test_opener.py +++ b/tests/editor/test_opener.py @@ -967,6 +967,95 @@ def boom(*a, **kw): text = "\n".join(r.getMessage() for r in caplog.records) flat = uri.replace("@ssh-remote+mac2", "") assert flat in text and "同名" in text + # 恒久化の方法 (空文字オプトアウト) もその場で示す。 + assert "DEVBASE_EDITOR_SSH_HOST=" in text + + +def test_open_editor_remote_ssh_empty_ssh_host_launches_flat_uri_with_context(monkeypatch, caplog): + """Remote-SSH + 解決した context でも DEVBASE_EDITOR_SSH_HOST= (空) ならフラット URI で launch。 + + 手元の VS Code に同名 context がある構成 (Windows → WSL の docker を直接) で、案内の + フラット URI を毎回手で貼らずに済ませる経路。settings.context は残す。 + """ + import logging + monkeypatch.setattr(opener.shutil, "which", lambda c: "/usr/bin/code") + + def boom(*a, **kw): + raise AssertionError("docker context show should not run") + + monkeypatch.setattr(opener.subprocess, "run", boom) + monkeypatch.setattr(opener, "_query_container_name", lambda *a, **kw: None) + calls = [] + with caplog.at_level(logging.INFO): + opener.open_editor( + project_name="investment", dev_service_name="dev", workdir="/work/investment", + environ={"VSCODE_IPC_HOOK_CLI": "/run/x.sock", + "SSH_CONNECTION": "192.168.1.16 5 192.168.1.201 22", + "DEVBASE_EDITOR_SSH_HOST": ""}, + isatty=True, ipc_alive=True, launcher=lambda cmd, env: calls.append(cmd), + docker_context="wsl", + ) + uri = calls[0][2] + assert "@ssh-remote+" not in uri and uri.endswith("/work/investment") + assert _decode(uri)["settings"]["context"] == "wsl" + text = "\n".join(r.getMessage() for r in caplog.records) + assert "同名" not in text # ネストしていないので案内は出ない + + +def test_open_editor_remote_ssh_context_from_docker_show_omits_opt_out_hint(monkeypatch, caplog): + """context が `docker context show` の推測だけのときは、空文字オプトアウトの案内を出さない。 + + ssh_host を外すと推測も行われず settings.context が消える (別の daemon へ繋ぐ) ため、 + 案内どおりにすると提示したフラット URI と違うものが開く。 + """ + import logging + monkeypatch.setattr(opener.shutil, "which", lambda c: "/usr/bin/code") + monkeypatch.setattr(opener, "_query_container_name", lambda *a, **kw: None) + monkeypatch.setattr(opener, "resolve_docker_context", + lambda env, default=None, **kw: default or "probed") + calls = [] + with caplog.at_level(logging.INFO): + opener.open_editor( + project_name="adminer", dev_service_name="dev", workdir="/work/adminer", + environ={"VSCODE_IPC_HOOK_CLI": "/run/x.sock", + "SSH_CONNECTION": "192.168.1.16 5 192.168.1.201 22", + "DEVBASE_EDITOR_SSH_HOST": "mac2"}, + isatty=True, ipc_alive=True, launcher=lambda cmd, env: calls.append(cmd), + ) + uri = calls[0][2] + assert "@ssh-remote+mac2" in uri and _decode(uri)["settings"]["context"] == "probed" + text = "\n".join(r.getMessage() for r in caplog.records) + assert "同名" in text and "DEVBASE_EDITOR_SSH_HOST=" not in text + + +def test_open_editor_remote_ssh_explicit_editor_context_survives_opt_out(monkeypatch, caplog): + """DEVBASE_EDITOR_DOCKER_CONTEXT の明示は、空文字オプトアウト後も settings.context に残る。""" + import logging + monkeypatch.setattr(opener.shutil, "which", lambda c: "/usr/bin/code") + monkeypatch.setattr(opener, "_query_container_name", lambda *a, **kw: None) + + def boom(*a, **kw): + raise AssertionError("docker context show should not run") + + monkeypatch.setattr(opener.subprocess, "run", boom) + base = {"VSCODE_IPC_HOOK_CLI": "/run/x.sock", + "SSH_CONNECTION": "192.168.1.16 5 192.168.1.201 22", + "DEVBASE_EDITOR_DOCKER_CONTEXT": "wsl"} + calls = [] + with caplog.at_level(logging.INFO): + opener.open_editor( + project_name="adminer", dev_service_name="dev", workdir="/work/adminer", + environ={**base, "DEVBASE_EDITOR_SSH_HOST": "mac2"}, + isatty=True, ipc_alive=True, launcher=lambda cmd, env: calls.append(cmd), + ) + assert "DEVBASE_EDITOR_SSH_HOST=" in "\n".join(r.getMessage() for r in caplog.records) + opener.open_editor( + project_name="adminer", dev_service_name="dev", workdir="/work/adminer", + environ={**base, "DEVBASE_EDITOR_SSH_HOST": ""}, + isatty=True, ipc_alive=True, launcher=lambda cmd, env: calls.append(cmd), + ) + flat = calls[1][2] + assert "@ssh-remote" not in flat and _decode(flat)["settings"]["context"] == "wsl" def test_open_editor_explicit_editor_context_beats_resolved(monkeypatch):