diff --git a/docs/glossary.md b/docs/glossary.md index 25ae9700..eb37b274 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -84,11 +84,11 @@ up の後に VS Code で dev コンテナを開く(docs/specifications/editor- | AWS の丸ごとの取り込み | — | ~/.aws/config と ~/.aws/credentials を切り出さずにそのまま 1 つの tar にして AWS_CONFIG_BASE64 へ書くこと。#314 より前の振る舞い | — | — | `docs/specifications/secret-backend.md` | | AWS のプロファイルの連なり | — | 選んだ AWS のプロファイルが動くのに要る節。sso_session が指す [sso-session <名前>] と、source_profile が指すプロファイル(たどれる限り) | — | — | `docs/specifications/secret-backend.md` | | グループ名の例 | — | エラー文・使い方の文言がグループ名の書き方として示す名前。lib/devbase/env/groups.py の定義 1 か所から出す | — | — | `docs/specifications/secret-backend.md` | -| プロファイル名の正規化 | — | GCP の鍵ファイル名の拡張子を除いた部分のうち [A-Za-z0-9_] 以外を _ へ置き換え、GCP のプロファイル名(GCP_CREDENTIALS_BASE64_<名前> の <名前>)にすること | — | — | — | -| ホストの region | — | ホストの ~/.aws/config の、選んだ AWS のプロファイルの節にある region の値。方法 3(Access Key)は [default]、方法 2(SSO Profile)は利用者が入れたプロファイル | — | — | — | -| region の出所 | — | 参照の AWS_DEFAULT_REGION へ書く値をどこから得たか。参照に既にある・ホストの region・利用者の入力・既定の値(ap-northeast-1)の 4 つ | — | — | — | -| 鍵の確認の結果 | — | AWS の方法 3 で、ホストの [default] の鍵を取り込むかを決めた結果。受け入れた・断った・確認しなかった(ホストに鍵が無い)の 3 つ。方針が import なら受け入れた。skip ならホストに鍵があれば断った、無ければ確認しなかったになる | — | — | — | -| 鍵ファイルの発見 | — | ~/gcp-credentials/ の *.json を読み、正規化したプロファイル名から鍵ファイルへの対応を作ること。出力を持たない | — | — | — | +| プロファイル名の正規化 | — | GCP の鍵ファイル名の拡張子を除いた部分のうち [A-Za-z0-9_] 以外を _ へ置き換え、GCP のプロファイル名(GCP_CREDENTIALS_BASE64_<名前> の <名前>)にすること | — | — | `docs/specifications/secret-backend.md` | +| ホストの region | — | ホストの ~/.aws/config の、選んだ AWS のプロファイルの節にある region の値。方法 3(Access Key)は [default]、方法 2(SSO Profile)は利用者が入れたプロファイル | — | — | `docs/specifications/secret-backend.md` | +| region の出所 | — | 参照の AWS_DEFAULT_REGION へ書く値をどこから得たか。参照に既にある・ホストの region・利用者の入力・既定の値(ap-northeast-1)の 4 つ | — | — | `docs/specifications/secret-backend.md` | +| 鍵の確認の結果 | — | AWS の方法 3 で、ホストの [default] の鍵を取り込むかを決めた結果。受け入れた・断った・確認しなかった(ホストに鍵が無い)の 3 つ。方針が import なら受け入れた。skip ならホストに鍵があれば断った、無ければ確認しなかったになる | — | — | `docs/specifications/secret-backend.md` | +| 鍵ファイルの発見 | — | ~/gcp-credentials/ の *.json を読み、正規化したプロファイル名から鍵ファイルへの対応を作ること。出力を持たない | — | — | `docs/specifications/secret-backend.md` | ## スナップショット(`snapshot`) @@ -188,16 +188,16 @@ base イメージの entrypoint が起動のたびに行う用意(containers/b | 語 | 識別子 | 意味 | 廃止した語 | 廃止した識別子 | 正本 | | --- | --- | --- | --- | --- | --- | -| 共通のボリューム | — | すべてのコンテナが /persistent/ai にマウントする 1 つのボリューム。グループをまたいで共有する | — | — | — | -| 共有のリンク | — | グループのボリュームの .claude の下にあり、共通のボリュームの同じ名前のエントリを指す symlink。plugins / skills / commands / CLAUDE.md / settings.json の 5 本。同じグループのすべてのコンテナが同じ 1 本を使う | — | — | — | -| リンクの段 | — | entrypoint が AI 設定のリンクを張る処理(devbase_setup_ai_settings の 1 回の実行) | — | — | — | -| 同時起動 | — | 2 つ以上のコンテナのリンクの段が、時間の上で重なって走ること | — | — | — | -| 修正前の手順 | — | #357 より前のリンクの張り方。「正しいかを見る → 違えば消す → ln -s」を、ほかのコンテナを待たずに行う | — | — | — | -| リンクの段のロック | — | 共通のボリュームのルートのディレクトリに対して取る排他のロック。修正後のリンクの段どうしを 1 つずつ順に走らせる。ボリュームにファイルを作らない | — | — | — | -| ロックの待ち時間の上限 | — | リンクの段のロックを待つ秒数の上限(10 秒)。超えたら警告を出してロックなしで進む | — | — | — | -| 張り替えの試行 | — | リンクを「消す → 張る → 読み直して確かめる」1 回ぶん。正しくなるまで試行の上限(20 回)まで繰り返し、張る操作の成否ではなく読み直した結果で成功を決める | — | — | — | -| 入れ子のリンク | — | リンクの位置がディレクトリを指すリンクになっているときに、張る操作がその先のディレクトリの中へ作るリンク(例: 共通のボリュームの plugins/plugins)。修正前の手順が同時起動で作ることがある | — | — | — | -| 排他の作成 | — | エントリを「無いときだけ作る」操作。同時に呼んでも作れるのは 1 つのプロセスだけで、作れたプロセスだけがそのエントリの中身を書く。退避とプレースホルダが使う | — | — | — | +| 共通のボリューム | — | すべてのコンテナが /persistent/ai にマウントする 1 つのボリューム。グループをまたいで共有する | — | — | `docs/specifications/container-link-stage.md` | +| 共有のリンク | — | グループのボリュームの .claude の下にあり、共通のボリュームの同じ名前のエントリを指す symlink。plugins / skills / commands / CLAUDE.md / settings.json の 5 本。同じグループのすべてのコンテナが同じ 1 本を使う | — | — | `docs/specifications/container-link-stage.md` | +| リンクの段 | — | entrypoint が AI 設定のリンクを張る処理(devbase_setup_ai_settings の 1 回の実行) | — | — | `docs/specifications/container-link-stage.md` | +| 同時起動 | — | 2 つ以上のコンテナのリンクの段が、時間の上で重なって走ること | — | — | `docs/specifications/container-link-stage.md` | +| 修正前の手順 | — | #357 より前のリンクの張り方。「正しいかを見る → 違えば消す → ln -s」を、ほかのコンテナを待たずに行う | — | — | `docs/specifications/container-link-stage.md` | +| リンクの段のロック | — | 共通のボリュームのルートのディレクトリに対して取る排他のロック。修正後のリンクの段どうしを 1 つずつ順に走らせる。ボリュームにファイルを作らない | — | — | `docs/specifications/container-link-stage.md` | +| ロックの待ち時間の上限 | — | リンクの段のロックを待つ秒数の上限(10 秒)。超えたら警告を出してロックなしで進む | — | — | `docs/specifications/container-link-stage.md` | +| 張り替えの試行 | — | リンクを「消す → 張る → 読み直して確かめる」1 回ぶん。正しくなるまで試行の上限(20 回)まで繰り返し、張る操作の成否ではなく読み直した結果で成功を決める | — | — | `docs/specifications/container-link-stage.md` | +| 入れ子のリンク | — | リンクの位置がディレクトリを指すリンクになっているときに、張る操作がその先のディレクトリの中へ作るリンク(例: 共通のボリュームの plugins/plugins)。修正前の手順が同時起動で作ることがある | — | — | `docs/specifications/container-link-stage.md` | +| 排他の作成 | — | エントリを「無いときだけ作る」操作。同時に呼んでも作れるのは 1 つのプロセスだけで、作れたプロセスだけがそのエントリの中身を書く。退避とプレースホルダが使う | — | — | `docs/specifications/container-link-stage.md` | ## 起動の後の処理(`post-start`) @@ -205,14 +205,14 @@ devbase up / scale がコンテナを起動して待ち、その後にホスト | 語 | 識別子 | 意味 | 廃止した語 | 廃止した識別子 | 正本 | | --- | --- | --- | --- | --- | --- | -| 起動の後の処理 | — | up と scale が、起動の待ちの後にホストの側から行う処理の並び。不足リポジトリの報告・./deploy の実行・token の配布・窓のタイトルの設定・エディタの自動オープンの 5 つ | — | — | — | -| インスタンスごとの処理 | — | 起動の後の処理のうち、インスタンスの 1 つ 1 つに行う 4 つ(エディタの自動オープンを除いたもの) | — | — | — | -| 起動の待ち | — | up と scale が、起動したインスタンスの entrypoint の完了を、制限時間まで待つ段 | — | — | — | -| 起動できたインスタンス | — | 起動の待ちで、entrypoint の完了を確かめられたインスタンス | — | — | — | -| 起動できなかったインスタンス | — | 起動の待ちの間に終了した・見つからない・制限時間までに entrypoint の完了を確かめられなかった、のどれかに当たるインスタンス | — | — | — | -| 増やしたインスタンス | — | devbase scale が足した番号のインスタンス(前の scale の値 + 1 から、新しい scale の値まで) | — | — | — | -| 後処理の対象 | — | インスタンスごとの処理を行うインスタンスの集合。up は起動できたインスタンス、scale は増やしたインスタンスのうち起動できたもの、やり直しは動いていて entrypoint の完了の印を確かめられたインスタンス | — | — | — | -| 起動の結果 | — | 起動の待ちが決めた、インスタンスごとの「起動できた」「起動できなかった(理由つき)」の内訳 | — | — | — | -| 起動できなかった理由 | — | 起動できなかったインスタンスに 1 つ付く区分。終了した・見つからない・時間切れの 3 つ | — | — | — | -| 起動の後の処理の段 | — | 後処理の対象を受けて、インスタンスごとの処理を決まった順に行う 1 つの関数 | — | — | — | -| 起動の後の処理のやり直し | — | 動いていて entrypoint の完了の印を確かめられたインスタンスへ、インスタンスごとの処理から ./deploy を除いた 3 つを行い直すコマンド(devbase project post-start) | — | — | — | +| 起動の後の処理 | — | up と scale が、起動の待ちの後にホストの側から行う処理の並び。不足リポジトリの報告・./deploy の実行・token の配布・窓のタイトルの設定・エディタの自動オープンの 5 つ | — | — | `docs/specifications/post-start.md` | +| インスタンスごとの処理 | — | 起動の後の処理のうち、インスタンスの 1 つ 1 つに行う 4 つ(エディタの自動オープンを除いたもの) | — | — | `docs/specifications/post-start.md` | +| 起動の待ち | — | up と scale が、起動したインスタンスの entrypoint の完了を、制限時間まで待つ段 | — | — | `docs/specifications/post-start.md` | +| 起動できたインスタンス | — | 起動の待ちで、entrypoint の完了を確かめられたインスタンス | — | — | `docs/specifications/post-start.md` | +| 起動できなかったインスタンス | — | 起動の待ちの間に終了した・見つからない・制限時間までに entrypoint の完了を確かめられなかった、のどれかに当たるインスタンス | — | — | `docs/specifications/post-start.md` | +| 増やしたインスタンス | — | devbase scale が足した番号のインスタンス(前の scale の値 + 1 から、新しい scale の値まで) | — | — | `docs/specifications/post-start.md` | +| 後処理の対象 | — | インスタンスごとの処理を行うインスタンスの集合。up は起動できたインスタンス、scale は増やしたインスタンスのうち起動できたもの、やり直しは動いていて entrypoint の完了の印を確かめられたインスタンス | — | — | `docs/specifications/post-start.md` | +| 起動の結果 | — | 起動の待ちが決めた、インスタンスごとの「起動できた」「起動できなかった(理由つき)」の内訳 | — | — | `docs/specifications/post-start.md` | +| 起動できなかった理由 | — | 起動できなかったインスタンスに 1 つ付く区分。終了した・見つからない・時間切れの 3 つ | — | — | `docs/specifications/post-start.md` | +| 起動の後の処理の段 | — | 後処理の対象を受けて、インスタンスごとの処理を決まった順に行う 1 つの関数 | — | — | `docs/specifications/post-start.md` | +| 起動の後の処理のやり直し | — | 動いていて entrypoint の完了の印を確かめられたインスタンスへ、インスタンスごとの処理から ./deploy を除いた 3 つを行い直すコマンド(devbase project post-start) | — | — | `docs/specifications/post-start.md` | diff --git a/docs/glossary/glossary.json b/docs/glossary/glossary.json index 282c3c50..81c8d295 100644 --- a/docs/glossary/glossary.json +++ b/docs/glossary/glossary.json @@ -660,162 +660,157 @@ "term": "プロファイル名の正規化", "context": "secret", "meaning": "GCP の鍵ファイル名の拡張子を除いた部分のうち [A-Za-z0-9_] 以外を _ へ置き換え、GCP のプロファイル名(GCP_CREDENTIALS_BASE64_<名前> の <名前>)にすること", - "source": "", - "pending_source": "issues/issue-334-design.md" + "source": "docs/specifications/secret-backend.md" }, { "term": "ホストの region", "context": "secret", "meaning": "ホストの ~/.aws/config の、選んだ AWS のプロファイルの節にある region の値。方法 3(Access Key)は [default]、方法 2(SSO Profile)は利用者が入れたプロファイル", - "source": "", - "pending_source": "issues/issue-334-design.md" + "source": "docs/specifications/secret-backend.md" }, { "term": "region の出所", "context": "secret", "meaning": "参照の AWS_DEFAULT_REGION へ書く値をどこから得たか。参照に既にある・ホストの region・利用者の入力・既定の値(ap-northeast-1)の 4 つ", - "source": "", - "pending_source": "issues/issue-334-design.md" + "source": "docs/specifications/secret-backend.md" }, { "term": "鍵の確認の結果", "context": "secret", "meaning": "AWS の方法 3 で、ホストの [default] の鍵を取り込むかを決めた結果。受け入れた・断った・確認しなかった(ホストに鍵が無い)の 3 つ。方針が import なら受け入れた。skip ならホストに鍵があれば断った、無ければ確認しなかったになる", - "source": "", - "pending_source": "issues/issue-334-design.md" + "source": "docs/specifications/secret-backend.md" }, { "term": "鍵ファイルの発見", "context": "secret", "meaning": "~/gcp-credentials/ の *.json を読み、正規化したプロファイル名から鍵ファイルへの対応を作ること。出力を持たない", - "source": "", - "pending_source": "issues/issue-334-design.md" + "source": "docs/specifications/secret-backend.md" }, { "term": "起動の後の処理", "context": "post-start", "meaning": "up と scale が、起動の待ちの後にホストの側から行う処理の並び。不足リポジトリの報告・./deploy の実行・token の配布・窓のタイトルの設定・エディタの自動オープンの 5 つ", - "pending_source": "issues/issue-224-requirements.md" + "source": "docs/specifications/post-start.md" }, { "term": "インスタンスごとの処理", "context": "post-start", "meaning": "起動の後の処理のうち、インスタンスの 1 つ 1 つに行う 4 つ(エディタの自動オープンを除いたもの)", - "pending_source": "issues/issue-224-requirements.md" + "source": "docs/specifications/post-start.md" }, { "term": "起動の待ち", "context": "post-start", "meaning": "up と scale が、起動したインスタンスの entrypoint の完了を、制限時間まで待つ段", - "pending_source": "issues/issue-224-requirements.md" + "source": "docs/specifications/post-start.md" }, { "term": "起動できたインスタンス", "context": "post-start", "meaning": "起動の待ちで、entrypoint の完了を確かめられたインスタンス", - "pending_source": "issues/issue-224-requirements.md" + "source": "docs/specifications/post-start.md" }, { "term": "起動できなかったインスタンス", "context": "post-start", "meaning": "起動の待ちの間に終了した・見つからない・制限時間までに entrypoint の完了を確かめられなかった、のどれかに当たるインスタンス", - "pending_source": "issues/issue-224-requirements.md" + "source": "docs/specifications/post-start.md" }, { "term": "増やしたインスタンス", "context": "post-start", "meaning": "devbase scale が足した番号のインスタンス(前の scale の値 + 1 から、新しい scale の値まで)", - "pending_source": "issues/issue-224-requirements.md" + "source": "docs/specifications/post-start.md" }, { "term": "後処理の対象", "context": "post-start", "meaning": "インスタンスごとの処理を行うインスタンスの集合。up は起動できたインスタンス、scale は増やしたインスタンスのうち起動できたもの、やり直しは動いていて entrypoint の完了の印を確かめられたインスタンス", - "pending_source": "issues/issue-224-requirements.md" + "source": "docs/specifications/post-start.md" }, { "term": "起動の結果", "context": "post-start", "meaning": "起動の待ちが決めた、インスタンスごとの「起動できた」「起動できなかった(理由つき)」の内訳", - "pending_source": "issues/issue-224-design.md" + "source": "docs/specifications/post-start.md" }, { "term": "起動できなかった理由", "context": "post-start", "meaning": "起動できなかったインスタンスに 1 つ付く区分。終了した・見つからない・時間切れの 3 つ", - "pending_source": "issues/issue-224-design.md" + "source": "docs/specifications/post-start.md" }, { "term": "起動の後の処理の段", "context": "post-start", "meaning": "後処理の対象を受けて、インスタンスごとの処理を決まった順に行う 1 つの関数", - "pending_source": "issues/issue-224-design.md" + "source": "docs/specifications/post-start.md" }, { "term": "起動の後の処理のやり直し", "context": "post-start", "meaning": "動いていて entrypoint の完了の印を確かめられたインスタンスへ、インスタンスごとの処理から ./deploy を除いた 3 つを行い直すコマンド(devbase project post-start)", - "pending_source": "issues/issue-224-design.md" + "source": "docs/specifications/post-start.md" }, { "term": "共通のボリューム", "context": "container-start", "meaning": "すべてのコンテナが /persistent/ai にマウントする 1 つのボリューム。グループをまたいで共有する", - "pending_source": "issues/issue-357-requirements.md" + "source": "docs/specifications/container-link-stage.md" }, { "term": "共有のリンク", "context": "container-start", "meaning": "グループのボリュームの .claude の下にあり、共通のボリュームの同じ名前のエントリを指す symlink。plugins / skills / commands / CLAUDE.md / settings.json の 5 本。同じグループのすべてのコンテナが同じ 1 本を使う", - "pending_source": "issues/issue-357-requirements.md" + "source": "docs/specifications/container-link-stage.md" }, { "term": "リンクの段", "context": "container-start", "meaning": "entrypoint が AI 設定のリンクを張る処理(devbase_setup_ai_settings の 1 回の実行)", - "pending_source": "issues/issue-357-requirements.md" + "source": "docs/specifications/container-link-stage.md" }, { "term": "同時起動", "context": "container-start", "meaning": "2 つ以上のコンテナのリンクの段が、時間の上で重なって走ること", - "pending_source": "issues/issue-357-requirements.md" + "source": "docs/specifications/container-link-stage.md" }, { "term": "修正前の手順", "context": "container-start", "meaning": "#357 より前のリンクの張り方。「正しいかを見る → 違えば消す → ln -s」を、ほかのコンテナを待たずに行う", - "pending_source": "issues/issue-357-requirements.md" + "source": "docs/specifications/container-link-stage.md" }, { "term": "リンクの段のロック", "context": "container-start", "meaning": "共通のボリュームのルートのディレクトリに対して取る排他のロック。修正後のリンクの段どうしを 1 つずつ順に走らせる。ボリュームにファイルを作らない", - "pending_source": "issues/issue-357-design.md" + "source": "docs/specifications/container-link-stage.md" }, { "term": "ロックの待ち時間の上限", "context": "container-start", "meaning": "リンクの段のロックを待つ秒数の上限(10 秒)。超えたら警告を出してロックなしで進む", - "pending_source": "issues/issue-357-design.md" + "source": "docs/specifications/container-link-stage.md" }, { "term": "張り替えの試行", "context": "container-start", "meaning": "リンクを「消す → 張る → 読み直して確かめる」1 回ぶん。正しくなるまで試行の上限(20 回)まで繰り返し、張る操作の成否ではなく読み直した結果で成功を決める", - "pending_source": "issues/issue-357-design.md" + "source": "docs/specifications/container-link-stage.md" }, { "term": "入れ子のリンク", "context": "container-start", "meaning": "リンクの位置がディレクトリを指すリンクになっているときに、張る操作がその先のディレクトリの中へ作るリンク(例: 共通のボリュームの plugins/plugins)。修正前の手順が同時起動で作ることがある", - "pending_source": "issues/issue-357-design.md" + "source": "docs/specifications/container-link-stage.md" }, { "term": "排他の作成", "context": "container-start", "meaning": "エントリを「無いときだけ作る」操作。同時に呼んでも作れるのは 1 つのプロセスだけで、作れたプロセスだけがそのエントリの中身を書く。退避とプレースホルダが使う", - "pending_source": "issues/issue-357-design.md" + "source": "docs/specifications/container-link-stage.md" } ] } diff --git a/docs/specifications/container-link-stage.md b/docs/specifications/container-link-stage.md new file mode 100644 index 00000000..ad278457 --- /dev/null +++ b/docs/specifications/container-link-stage.md @@ -0,0 +1,275 @@ +# 同時起動でもリンクの段が失敗しない張り方(entrypoint の AI 設定のリンク) + +## 概要 + +base イメージの entrypoint(`containers/base/entrypoint.sh`)は、起動のたびに AI 設定のリンクを張る(リンクの段)。 +同じアカウントグループのコンテナは、グループのボリュームの `.claude` の下にある共有のリンク 5 本 +(`plugins` / `skills` / `commands` / `CLAUDE.md` / `settings.json`)を同じ 1 本ずつ使うため、ホストの再起動の後の +一斉起動や、`scale` が 2 以上のプロジェクトの `devbase up` では、リンクの段が時間の上で重なる(同時起動)。 + +リンクの段は、同時起動でも、変更前のイメージのコンテナと並んでも失敗せず、1 つずつ順に走らせたときと同じ状態を +2 つのボリュームに残す。そのために次の 3 つを持つ。 + +- **リンクの段のロック**: 共通のボリュームのルートのディレクトリに `flock` を掛け、リンクの段どうしを 1 つずつ順に走らせる +- **張り替えの試行**: リンクを張る操作の成否ではなく、張った後に読み直した結果で成功を決める +- **排他の作成**: 退避とプレースホルダは、エントリを「無いときだけ作る」操作で作れた 1 つのプロセスだけが中身を書く + +コマンド・環境変数・compose の生成物は持たない。効くのは base イメージを建て直した後のコンテナからである。 + +## 用語 + +語の定義は [用語集: コンテナの起動(`container-start`)](../glossary.md#コンテナの起動container-start) にある。 +使う語: 共通のボリューム・共有のリンク・リンクの段・同時起動・修正前の手順・リンクの段のロック・ロックの待ち時間の上限・ +張り替えの試行・入れ子のリンク・排他の作成。アカウントグループとグループのボリュームは +[機密の置き場(`secret`)](../glossary.md#機密の置き場secret) の語をそのままの意味で使う。 + +この仕様は `container-start` のコンテキストで閉じる。コンテナを起動する側(`devbase up` / `devbase scale`)には +触れない。 + +## 構成要素 + +| 要素 | 置き場所(`containers/base/entrypoint.sh`) | 責務 | +| --- | --- | --- | +| リンクの段の関数 | `devbase_setup_ai_settings` | 2 つのボリュームのルートを用意し、サブシェルの中でリンクの段のロックを取って本体を呼ぶ。本体の終了コードをそのまま返す | +| リンクの段の本体 | `devbase_apply_ai_settings` | 退避(イメージの `~/.claude` の初期設定を共通のボリュームへ、Kiro CLI の認証の状態をグループのボリュームへ)・ホームのリンク・共有のリンクを、この順で行う | +| リンクを張る関数 | `devbase_link_setting` | リンクの先のプレースホルダを用意し、張り替えの試行を繰り返す | +| リンクの判定 | `devbase_link_is_set` | リンクの位置が、渡した先を指す symlink かを終了コード(0 / 1)で返す。何も出力しない | +| 退避の 1 エントリ | `devbase_seed_entry` | コピー先のエントリを排他の作成で作り、作れたときだけ中身をコピーする | +| プレースホルダの作成 | `devbase_ensure_entry` | 無いエントリを排他の作成で作り、作れたときだけ中身(JSON のファイルは `{}`)を書く | +| 排他の作成 | `devbase_create_entry` | エントリを無いときだけ作り、結果を終了コード(作れた 0 / 既にあった 1 / 作れない 2)で返す | +| 上限の定数 | `DEVBASE_LINK_LOCK_WAIT`(10)・`DEVBASE_LINK_TRIES`(20) | ロックの待ち時間の上限(秒)と試行の上限(回) | + +関数と定数は `DEVBASE_ENTRYPOINT_LIB_ONLY` の `return` より前に置く。テストが関数だけを読み込んで走らせるためである。 +リンクを張る関数は、ホームのリンク(`~/.shellrc.d` などを含む。[shellrc-dir.md](shellrc-dir.md))にも共有のリンクにも +同じものを使う。分けると、位置に実体があるときの扱いを 2 か所で保つことになる。ホームのリンクでは試行が 1 回で終わる。 + +```mermaid +graph TD + M[entrypoint の本文] -->|呼ぶ| S[リンクの段の関数] + S -->|ロックを取って呼ぶ| A[リンクの段の本体] + A -->|退避するエントリを選ぶ| X[devbase_seed_image_claude_settings] + X -->|エントリごとに呼ぶ| E[退避の 1 エントリ] + A -->|グループのボリュームへ取り込む| E + A -->|リンクごとに呼ぶ| L[リンクを張る関数] + L -->|プレースホルダ| P[プレースホルダの作成] + E -->|無いときだけ作る| R[排他の作成] + P -->|無いときだけ作る| R + L -->|読み直す| C[リンクの判定] +``` + +## 仕様 + +### 集約 + +| 集約 | 持ち主 | 根 | エンティティ | 値オブジェクト | +| --- | --- | --- | --- | --- | +| 共有のリンク | リンクを張る関数(`devbase_link_setting`) | 共有のリンク(グループのボリュームの `.claude` の下の位置で識別する) | — | リンクの先(パスの文字列)・位置の状態(無い / 正しいリンク / 別の先を指すリンク / 実体) | +| リンクの段 | リンクの段の関数(`devbase_setup_ai_settings`) | リンクの段の 1 回の実行 | 張り替えの試行(リンクごとの何回目かで識別する) | ロックの結果(取れた / 待ち時間の上限を超えた / `flock` が無い)・排他の作成の結果(作れた / 既にあった / 作れない)・2 つの上限 | + +共有のリンクの持ち主は関数として 1 つだが、同じグループのコンテナの数だけ別々のプロセスで動く。変更前のイメージの +コンテナも、ロックを取らずに同じ位置を書く(修正前の手順)。下の条件は、ほかのプロセスが同じ位置を書き換える +ことを前提に保つ。 + +### リンクの段 + +```mermaid +sequenceDiagram + participant S as リンクの段の関数 + participant A as リンクの段の本体 + participant L as リンクを張る関数 + participant V as 2 つのボリューム + S->>V: ルートを用意する + alt flock がある + S->>V: 共通のボリュームのルートにロックを掛ける(最長 10 秒) + alt 待ち時間の上限を超えた + S->>S: 警告の行を出し、ロックなしで進む + end + else flock が無い + S->>S: ロックなしで進む + end + S->>A: 呼ぶ + A->>V: 退避する + loop ホームのリンクと共有のリンク + A->>L: リンクの位置と先 + L->>V: プレースホルダを用意し、張り替えの試行を繰り返す + alt 正しくなった + L-->>A: 0 + else 試行の上限を超えた + L-->>A: ERROR の行・1(本体はここで止まる) + end + end + A-->>S: 本体の終了コード + S->>V: ロックを手放す(サブシェルを抜ける) +``` + +- ルートの用意(`devbase_ensure_persistent_root`)はロックの前に行う。ロックを掛ける相手のディレクトリが要るため + である。中身は「無ければ作る・書けなければ所有者を直す」で、重なっても結果は変わらない +- ロックは共通のボリュームのルートのディレクトリそのものに掛け、ロックのためのファイルを作らない。退避が書く共通の + ボリュームはすべてのコンテナが共有するため、ここに 1 つ置けば、グループのボリュームの処理も含めてリンクの段の全体が + 順に走る。ボリュームに残るものが無く、変更前の手順とスナップショットから見える並びが変わらない。別のグループの + コンテナも順番を待つが、リンクの段 1 回は 0.1 秒ほどである +- ロックはサブシェルにだけ付けた fd 9(`exec 9<"$ai_root"`)で持ち、同じサブシェルの中で本体を呼ぶ。サブシェルを + 抜ければ成功でも失敗でも fd が閉じてロックが外れ、entrypoint が最後に `exec` するコマンドへ引き継がない。 + 持ったプロセスが kill されたときはカーネルが外す +- ロックは 10 秒まで待ち、取れなければ警告を出してロックなしで進む。止まったまま終わらない持ち主が 1 つあるだけで、 + ホストのすべてのコンテナの起動が止まるのを避けるためである。ロックなしでも、共有のリンクは張り替えの試行で正しく + なり、退避とプレースホルダは排他の作成で 1 つのプロセスだけが書く。失うのは、リンクの段の全体が 1 つずつ順に走る + 保証だけである。10 秒は 8 個が順に走る所要(実測で 2 秒未満)より長い +- `flock` が無い環境(開発者の macOS)でも止めずにロックなしで進む + +### 張り替えの試行 + +```mermaid +graph TD + P[リンクの先の
プレースホルダを用意する] --> Q{最初から
正しいか} + Q -->|はい| OK1([✓ の行を出して 0]) + Q -->|いいえ| R[位置に何かあれば消す: rm -rf] + R --> T[張る: ln -sn] + T --> W{読み直すと
正しいか} + W -->|はい| OK2([所有者を揃えて 0]) + W -->|いいえ| N{20 回
試したか} + N -->|いいえ| R + N -->|はい| NG([ERROR の行を出して 1]) +``` + +- 消す操作と張る操作は、失敗してもそこで終わらない。標準エラーは捨てずに控え、最後の ERROR の行にだけ使う +- 張る操作には `-n` を付ける。`-n` の無い `ln -s` は、位置が既にディレクトリを指すリンクだと失敗せず、その先の + ディレクトリの中へリンクを作って 0 で終わる(入れ子のリンク。例: 共通のボリュームの `plugins/plugins`)。`-n` を + 付けると位置そのものを対象にして失敗し、読み直しへ進む +- 試行の間に待ち時間は置かない。次の試行が要るのは、相手のプロセスが位置を書き換えたときだけである +- 一時的な名前で張ってから `mv -T` で置き換える方式は採らない。位置が実体のディレクトリのときに置き換えられず、 + `mv -T` は macOS に無く、途中で kill されると一時的な名前がボリュームに残るためである +- リンクの位置に実体(通常のファイル・ディレクトリ・別の先を指すリンク)があるときは、消して張り直す + +共有のリンクの位置が取る状態と遷移は次のとおりである。「別の先を指すリンク」は、指す先の無い壊れたリンクを含む。 +遅れて届いた「消す」は、正しくなる前に「違う」と判定を済ませていたプロセス(修正前の手順の相手と、ロックなしで +進んだリンクの段)が出す。 + +```mermaid +stateDiagram-v2 + [*] --> 無い: 新しいグループのボリューム + 無い --> 正しいリンク: 張る + 別の先を指すリンク --> 無い: 消す + 実体 --> 無い: 消す + 正しいリンク --> 無い: 遅れて届いた「消す」 + 正しいリンク --> 実体: 運用中の置き換え(引き金は分かっていない) + 正しいリンク --> [*]: リンクの段が終わる +``` + +修正前の手順で張る相手と重なったときは次のとおり進む。修正前の手順の相手が消すのは 1 プロセスにつき 1 回で、 +同時起動が 8 個なら試行は 9 回までに収まる。試行の上限の 20 回は、その 2 倍の余裕である。 + +| 重なり方 | リンクの段の側 | 修正前の手順の相手 | +| --- | --- | --- | +| 相手が先に張り、その後でこちらが張る | 張る操作は失敗するが、読み直すと正しいので 0 | 0 | +| こちらが張った後で、相手が消して張る | 読み直した時点で正しければ 0。消された直後なら次の試行で張る | 0 | +| こちらが張った後で、消し終えていた相手が張る | 0 | ファイルのリンクでは非 0。ディレクトリのリンクでは入れ子のリンクを作って 0 | + +### 退避とプレースホルダ + +```mermaid +graph TD + Q{エントリは
既にあるか} -->|はい| SK([何もせず 0]) + Q -->|いいえ| C[排他の作成] + C -->|作れた| W[中身を書く] + W --> OK([0]) + C -->|既にあった| SK + C -->|作れず、読み直しても無い| NG([ERROR の行を出して非 0]) +``` + +- 通常のファイルは bash の noclobber(`set -C` を付けたサブシェルの `>`)で空のファイルとして作る。作れたプロセスが、 + その空のファイルへ中身を書く(退避は `cp -a` で上書きし、JSON のプレースホルダは `{}` を書く) +- ディレクトリは `perl` から `mkdir(2)` を呼んで作る。base イメージの `mkdir`(uutils coreutils)は、同時に呼ぶと + 2 個以上が 0 で終わり、作れたプロセスを 1 つに決められないため使わない。`perl` は base イメージに必ずある + Essential のパッケージ(perl-base)で、道具を足さない。`perl` が無い環境では `mkdir` で作り、止めない +- 作る操作の終了コードが非 0 のときは読み直して決める。エントリがあれば「既にあった」(1)、無ければ「作れない」(2) +- 退避でディレクトリの子を `cp -a` でコピーするのは、作れたプロセスだけである。既にある子のディレクトリをコピー先に + する `cp -a` が走らないため、子の中へ重ねてコピーしない(`plugins/<名前>/<名前>` を作らない) +- 作れなかったプロセスは、作れたプロセスが中身を書き終えるのを待たずにリンクへ進む + +### 起動ログ + +| 場合 | 行 | 出力先 | +| --- | --- | --- | +| リンクが最初から正しい | ` ✓ <リンクの位置> (symlink exists)` | 標準出力 | +| 位置に何かあり、消す | ` Removing existing <リンクの位置>...`(1 回目の試行でだけ出す) | 標準出力 | +| 張る | ` Creating symlink: <リンクの位置> -> <リンクの先>`(1 回目の試行でだけ出す) | 標準出力 | +| 張る操作か消す操作が失敗したが、読み直すと正しい | 何も足さない(`ln:` と `rm:` の行を出さない) | — | +| 試行の上限まで正しくならない | `ERROR: symlink を張れない: <リンクの位置> -> <リンクの先> (<最後に失敗したコマンドの出力>)` | 標準エラー | +| 排他の作成で作れなかったが、読み直すとエントリがある | 何も足さない | — | +| エントリを作れず、読み直しても無い | `ERROR: エントリを作れない: <エントリのパス> (<失敗したコマンドの出力>)` | 標準エラー | +| ロックを待ち切れなかった | `WARNING: リンクの段のロックを 10 秒待っても取れないため、ロックなしで進める: <共通のボリュームのルート>` | 標準エラー | + +終了コードは、リンクの段の関数もリンクを張る関数も成功が 0、失敗が非 0 である。entrypoint は `set -e` で動くため、 +リンクの段の関数が非 0 を返すとコンテナの起動が止まる。2 つの上限は環境変数から読まない(テストは関数を読み込んだ +後に定数を上書きして確かめる)。 + +### 常に成り立つ条件 + +- リンクを張る関数が 0 で返るのは、リンクの位置が正しい先を指す symlink であると読み直して確かめたときだけである。 + `ln` と `rm` の終了コードでは決めない +- 張る操作は、位置がディレクトリを指すリンクになっていても、その先のディレクトリの中へリンクを作らない +- 試行の上限(20 回)まで正しくならなければ、非 0 で返り、リンクの位置のパスを含む行を標準エラーへ出す +- 0 で返るとき、`ln:` で始まる行を出さない +- ロックを取れたリンクの段どうしは、退避・プレースホルダ・リンクの処理が時間の上で重ならない +- ロックを取れない(待ち時間の上限を超えた・`flock` が無い)ときは、止まらずに進み、上の条件と排他の作成の条件を守る +- ロックは、リンクの段を抜けた後(成功でも失敗でも)に残らず、entrypoint が最後に起動するコマンドへ引き継がない +- 2 つのボリュームに、ロックのためのファイルも、一時的な名前のエントリも作らない +- 退避とプレースホルダが 2 つのボリュームに作るエントリは、排他の作成で作れた 1 つのプロセスだけが中身を書く。 + ロックを取れたかどうかによらない + +どのホームにも同じ中身がある同時起動では、ロックを取れなくても、2 つのボリュームに残る並び・リンクの先・ファイルの +中身が、1 個だけ走らせた後と同じになる。 + +## 運用 + +- 反映には `devbase build base` で base イメージを建て直し、派生イメージも建て直す。建て直すまで、変更前のイメージの + コンテナは同時起動で落ちることと、入れ子のリンクを作ることが残る。同じグループに変更前と変更後のイメージの + コンテナが並んでも、変更後のコンテナは落ちない +- ロックを取れず、ホームの中身が違うコンテナが同時に退避するときは、エントリごとに排他の作成で作れた 1 つの + コンテナの中身が丸ごと残る(エントリによって作れたコンテナが違うことがある)。ロックを取れたときは起きない +- 排他の作成で作れたプロセスが中身を書き終える前に kill されると、書きかけのエントリが残り、次のリンクの段は + 「既にある」として退避しない(1 個だけで走らせて退避の途中で kill されたときと同じ) +- 初回の起動で派生イメージが `~/.claude` に大きなディレクトリを持ち、退避が 10 秒を超えると、待つ側はロックなしで進む。 + 共有のリンクは正しくなるが、起動の直後に共通のボリュームの中身が揃っていない時間がある +- 変更前の手順が過去に作った入れ子のリンクは片付けない +- リンクの段の外の entrypoint の処理(Git・AWS・GCP の資格情報、リポジトリの clone、Docker-in-Docker)はロックの外で動く + +## テスト観点 + +関数の水準のテストは Docker を使わず、別々のホームと同じ 2 つのボリューム(一時ディレクトリ)を渡したリンクの段を +同時に始める。1 回の同時実行では変更前の手順でも通ることがあるため、1 件の中で同時実行を繰り返し、1 回でも条件が +破れたら落ちる形にする(`tests/containers/test_entrypoint_ai_settings_concurrent.py`)。 + +- `settings.json` だけが通常のファイルの状態で 2 個・8 個同時に走らせると、全部が 0 で終わり、`settings.json` が共通の + ボリュームを指し、共通のボリュームの中身が変わらないこと +- 2 つのボリュームが空で、どのホームにも同じ中身がある状態で 8 個同時に走らせた後の並び・リンクの先・中身が、 + 1 個だけ走らせた後と同じであること。`flock` が無い状態でも同じであること +- グループのボリュームが空・`plugins` が実体のディレクトリ・別の先を指すリンク・壊れたリンクのそれぞれで 8 個同時に + 走らせると、全部が 0 で終わり、リンクが共通のボリュームを指すこと +- 修正前の手順で張る相手と同時に走らせても、こちらが 0 で終わり、共有のリンクが正しいこと。リンクの段の後に修正前の + 手順を同じボリュームで走らせても 0 で終わること +- 消した直後にほかのプロセスが同じリンクを張り終えた状態で 0 を返し、`plugins` の中に入れ子のリンクを作らないこと +- グループのボリュームの `.claude` に書き込めない・退避のコピー先の親に書き込めない状態で、非 0 で終わり、そのパスを + 含む行を標準エラーへ出すこと(root では状態を作れないため skip する) +- 0 で終わった出力に `ln:` で始まる行が無く、ボリュームにロックのためのエントリも一時的な名前のエントリも無いこと +- 排他の作成を 8 個同時に呼ぶと、通常のファイルでもディレクトリでも 0 を返すのは 1 個だけで、既にあるエントリには 1 を + 返して中身を変えないこと。`perl` が無い状態で 1 個だけ走らせた結果が、`perl` がある状態と同じであること +- ロックの持ち主がいる間は待ち、手放された後に進むこと。持ち主が手放さないときは待ち時間の上限の後に警告して 0 で + 終わること。持ち主が kill されたとき・張りかけで終わった後から走らせたときも 0 で終わること。リンクの段から戻った + 後にほかのプロセスが待たずにロックを取れること(`flock` が無い macOS では skip する) +- 8 個同時に走らせたとき 8 個すべてが 30 秒以内に終わること。1 個だけの所要(CPU 時間)が、修正前の手順の所要に + 1 秒を足した値を超えないこと +- 既存の `tests/containers/test_entrypoint_ai_settings.py` が書き換えずに通ること + +実物の base イメージでは、作業ツリーの `entrypoint.sh` を読み取り専用でマウントし、使い捨ての名前のボリューム 2 つで +コンテナ 8 個を同時に起動して、全部の entrypoint が 0 で終わることと、base イメージの道具(uutils の `ln` / `mkdir`、 +`perl`)で排他の作成が 1 つのプロセスにだけ通ることを確かめる(`tests/containers/test_base_image_link_stage.py`)。 +利用者のイメージのタグもボリュームも置き換えず、Docker かイメージが無ければ skip する(CI では skip になる)。 + +## 関連リンク + +- [用語集: コンテナの起動(`container-start`)](../glossary.md#コンテナの起動container-start) +- [機密ストアの保存先の差し替え(secret-backend.md)](secret-backend.md)(アカウントグループとグループのボリューム) +- [shellrc-dir.md](shellrc-dir.md)(同じリンクを張る関数とプレースホルダの作成を使う `~/.shellrc.d`) +- [起動の後の処理(post-start.md)](post-start.md)(1 台が起動できなかったときの `up` / `scale` の扱い) diff --git a/docs/specifications/post-start.md b/docs/specifications/post-start.md new file mode 100644 index 00000000..685ea117 --- /dev/null +++ b/docs/specifications/post-start.md @@ -0,0 +1,354 @@ +# 起動の後の処理(`up` / `scale` / `project post-start`) + +## 概要 + +`devbase up` と `devbase scale` は、コンテナを起動して entrypoint の完了を待った後、ホストの側から +決まった処理を行う。不足リポジトリの報告・`./deploy` の実行・token の配布・窓のタイトルの設定の 4 つを +インスタンスごとに同じ順で行い、`up` だけがその後でエディタを自動で開く。 + +処理を行うインスタンスの集合(後処理の対象)は、`up` / `scale` の本体が起動の結果から決め、1 つの段 +(`_run_post_start`)へ渡す。1 台が起動できなくても、起動できたインスタンスへは処理を届け、そのうえで +起動できなかった全部の名前・理由と補う手順を出して終了コード 1 で終える。動いているインスタンスへ、 +コンテナを作り直さずに処理だけを行い直すコマンドとして `devbase project post-start [name]` を持つ。 + +## 用語 + +語の定義は [用語集: 起動の後の処理(`post-start`)](../glossary.md#起動の後の処理post-start) にある。 +使う語: 起動の後の処理・インスタンスごとの処理・起動の待ち・起動できたインスタンス・起動できなかったインスタンス・ +増やしたインスタンス・後処理の対象・起動の結果・起動できなかった理由・起動の後の処理の段・起動の後の処理のやり直し。 +開発サービス名は [Compose の構成(`compose`)](../glossary.md#compose-の構成compose)、窓と動いているインスタンスは +[エディタで開く(`editor`)](../glossary.md#エディタで開くeditor) の語である。 + +この仕様の変更は `post-start` のコンテキストで閉じる。`editor`(窓のタイトルの設定・動いているインスタンスの調べ方)、 +`secret`(token の発行とコンテナへの書き込み)、`compose`(開発サービス名と生成物 `.docker-compose.scale.yml`)には +順応者として接し、それらのモデルを変えない。 + +## 構成要素 + +| 要素 | 置き場所 | 責務 | +| --- | --- | --- | +| 起動の待ち | `lib/devbase/utils/docker.py` の `wait_for_containers_ready` | 1 から台数までの番号を、巡ごとに状態と完了の印で確かめる。全部が起動できたら `True` を返し、起動できなかった番号があれば、残りを制限まで待った後に `ContainerStartupError` を投げる | +| 起動の結果の型 | 同ファイルの `StartupFailure`・`ContainerStartupError` | 起動できた番号と、起動できなかった番号ごとの理由・ログの末尾を運び、利用者へ出す文言を組み立てる | +| 起動の後の処理の段 | `lib/devbase/commands/container.py` の `_run_post_start` | 後処理の対象を受け、インスタンスごとの処理を決まった順に呼ぶ。対象を自分で決めず、番号を足しも引きもしない。`./deploy` を走らせるかを `run_deploy` で受ける | +| 個々の処理 | 同ファイルの `_report_missing_repos`・`_run_deploy_script_for_instances`・`_push_bao_token`・`_apply_window_titles` | キーワード引数 `indices`(対象の番号の並び)を受ける。渡されなければ番号 1 から台数まで(token の配布は開始の番号から)に行う | +| エディタの自動オープン | 同ファイルの `_maybe_open_editor` | キーワード引数 `started`(起動できた番号の並び)を受け、開く番号がそこに無ければ警告して開かない | +| `up` の本体 | 同ファイルの `cmd_up` | 起動の待ちの例外を受けて後処理の対象を決め、段とエディタの自動オープンを呼ぶ | +| `scale` の本体 | 同ファイルの `cmd_scale` | 同じく例外を受け、増やしたインスタンスのうち起動できたものを対象に段を呼ぶ | +| 部分的な起動の扱い | 同ファイルの `_start_or_partial`・`_warn_partial_start`・`_finish_partial`・`_report_startup_failure` | 起動の段の例外から起動できた番号を取り出す。待ちの直後の 1 行の警告と、出力の最後の報告を出す | +| やり直しのコマンド | 同ファイルの `cmd_post_start` | 接続先と機密を用意し、動いているインスタンスと完了の印から後処理の対象を決め、段を `./deploy` なしで呼ぶ | +| 動いているインスタンスの調べ方 | `lib/devbase/utils/docker.py` の `running_dev_instances` | `docker ps` をプロジェクトのラベルで絞り、`<開発サービス名>-<番号>` の `(番号, コンテナ名)` を返す。取得できなければ `None` | +| CLI の登録 | `lib/devbase/cli.py`(`SUBCMD_MAP` と `project` の parser)・`bin/devbase`(`_PROJECT_NAME_SUBCOMMANDS`)・`_dispatch_lifecycle`・`etc/devbase-completion.bash`・`etc/_devbase` | `project` グループの `post-start [name] [--context NAME]` | +| 完了の印 | `containers/base/entrypoint.sh` | 起動のはじめに `/tmp/entrypoint-ready` を消し、entrypoint の最後に置く | + +```mermaid +graph TD + CLI[CLI の登録] --> Up[up の本体] + CLI --> Scale[scale の本体] + CLI --> Redo[やり直しのコマンド] + Up --> Wait[起動の待ち] + Scale --> Wait + Up --> Stage[起動の後の処理の段] + Scale --> Stage + Redo --> Stage + Up --> Report[起動できなかったときの報告] + Scale --> Report + Stage --> Each[個々の処理と ./deploy] + Up --> Editor[エディタの自動オープン] +``` + +起動の待ちは、`up` では起動の段 `_run_deploy_pipeline`、`scale` では `_run_scale_pipeline` の最後に呼ばれる。 +2 つの起動の段は起動の待ちの例外を受けずにそのまま通し、本体が受ける。 + +## 仕様 + +### 集約 + +| 集約 | 持ち主 | 根 | エンティティ | 値オブジェクト | +| --- | --- | --- | --- | --- | +| 起動の結果 | 起動の待ち(`utils/docker.py`) | 1 回の待ちの結果 | インスタンス(番号で識別する) | 起動できなかった理由・ログの末尾 | +| 起動の後の処理の実行 | `up` / `scale` の本体・やり直しのコマンドと、起動の後の処理の段(`commands/container.py`) | 1 回の実行 | — | 後処理の対象(番号の集合)・処理の並び | + +後処理の対象を決めるのは `cmd_up`・`cmd_scale`・`cmd_post_start` だけである。どちらの集約も 1 回のコマンドの +中だけで生き、保存しない。窓のタイトルの設定ファイルと `~/.vault-token` は `editor` と `secret` の持ち物で、 +書き方はそれぞれの仕様に従う。 + +```mermaid +classDiagram + class DockerError + class ContainerStartupError { + +ready: tuple~int~ + +failures: tuple~StartupFailure~ + } + class StartupFailure { + +index: int + +service: str + +reason: str + +logs: str + +timeout: int + +describe() str + } + DockerError <|-- ContainerStartupError + ContainerStartupError "1" --> "1..*" StartupFailure +``` + +`ready` と `failures` は番号の昇順で、合わせると待った番号の全部になる。`reason` は `exited`・`not_found`・ +`timeout` のどれかで、`logs` は `exited` のときだけ中身を持つ。 + +### 起動の待ち + +`wait_for_containers_ready(container_prefix, scale, ready_file, timeout, compose_file)` は、1 から `scale` までの番号を +巡ごとに確かめる。1 巡では、まだ決まっていない番号ごとに状態の確認(`docker compose ps`)を 1 回、動いていれば +完了の確認(`docker compose exec -T <サービス> test -f /tmp/entrypoint-ready`)を 1 回行う。決まった番号へは、 +その後の巡で問い合わせない。決まっていない番号が残っていれば 1 秒休んで次の巡へ進み、巡の数が `timeout`(60)に +達したら終える。 + +```mermaid +stateDiagram-v2 + [*] --> 待っている + 待っている --> 待っている: 動いているが完了の印がまだ無い + 待っている --> 起動できた: 完了の印がある + 待っている --> 起動できなかった: 終了した(状態が exited か dead) + 待っている --> 起動できなかった: 見つからない(状態を取れない) + 待っている --> 起動できなかった: 時間切れ(巡の数が制限に達した) + 起動できた --> [*] + 起動できなかった --> [*] +``` + +| 起動できなかった理由 | 決まる条件 | 番号の行の文言 | +| --- | --- | --- | +| 終了した(`exited`) | 状態が `exited` か `dead` を含む | `exited unexpectedly` と、続く `Last logs:` とログの末尾(10 行まで。決まったときに 1 回だけ取る) | +| 見つからない(`not_found`) | 状態を取れない | `container not found` | +| 時間切れ(`timeout`) | 巡の数が制限に達した時点で、完了の印を確かめられていない | `timeout (<制限>s) waiting for the entrypoint to complete` | + +1 台が起動できなくても、残りの番号は制限まで待つ。番号の小さい 1 台が落ちたときにも、後から起動を終える +インスタンスへ後処理を届けるためである。そのため 1 台が落ちた `up` / `scale` が終わるまでの時間は、最長で +制限まで延びる。制限は 1 秒の休みを挟む巡の回数で数えるため、docker の呼び出しにかかる分だけ実際の時間は +60 秒を超える。 + +起動できなかった番号が 1 つ以上あれば `ContainerStartupError` を投げる。引数と成功時の戻り値は変えず、例外は +`DockerError`(`DevbaseError` の派生)なので、受けない呼び出し元は失敗のまま終わる。内訳を戻り値で返さないのは、 +戻り値を見ない呼び出し元が部分的な失敗を成功として扱えてしまうためである。文言は次の形である。 + +```text +Container startup failed: 2 of 3 instances did not become ready. + - dev-2: exited unexpectedly + Last logs: + <そのコンテナのログの末尾。10 行まで> + - dev-3: timeout (60s) waiting for the entrypoint to complete +``` + +完了の印(`/tmp/entrypoint-ready`)は、entrypoint が起動のはじめに消し、最後に置く。`docker start` で起こし直した +コンテナに前の起動の印が残り、初期化の途中でも完了と読まれることは無い。この振る舞いは base イメージを建て直した +後のコンテナから効く。 + +### 起動の後の処理の段 + +`_run_post_start(project_name, scale, indices, dev_service_name, config, compose_file, run_deploy=True)` は、 +後処理の対象 `indices` へ次の順で処理を行う。対象が空なら何も行わない(token も発行しない)。 + +| 順 | 処理 | 条件 | 失敗したとき | +| --- | --- | --- | --- | +| 1 | 不足リポジトリの報告(`Repositories missing in /work of <コンテナ>`) | 常に | 問い合わせに失敗すれば何も出さずに続ける | +| 2 | `./deploy` の実行(番号ごとに `DEVBASE_INSTANCE_INDEX` を渡す) | `run_deploy` が真で、`./deploy` があるとき | 非 0 は警告して次の番号へ続ける | +| 3 | token の配布(`~/.vault-token`) | backend が `openbao` のとき | 取れない・書けないときは警告して続ける | +| 4 | 窓のタイトルの設定 | `DEVBASE_WINDOW_TITLE` で止めていないとき | 書けないときは debug のログに出して続ける | + +個々の処理の失敗は終了コードへ反映しない。段は例外を投げない。`up` と `scale` は同じ順で処理し、やり直しは +同じ順から `./deploy` を除く。`scale` の順を `up` に合わせたのは、`up` の振る舞いを変えないためである(同じ +`./deploy` は `up` では token の無い状態で走っており、token があることを前提にできない)。 + +### `up` と `scale` + +```mermaid +sequenceDiagram + participant U as 利用者 + participant C as up / scale の本体 + participant W as 起動の待ち + participant P as 起動の後の処理の段 + U->>C: devbase up / devbase scale N + C->>C: 構成の生成と起動 + break docker compose up が非 0 + C-->>U: 失敗(後処理なし・終了コード 1) + end + C->>W: 1 から台数までを待つ + alt 全部が起動できた + W-->>C: 戻る + else 起動できなかったものがある + W-->>C: ContainerStartupError + C-->>U: 警告(名前と、後処理を続けるか) + end + C->>C: 後処理の対象を決める + C->>P: 対象の番号 + P->>P: 不足リポジトリの報告 → ./deploy → token の配布 → 窓のタイトルの設定 + C->>C: エディタの自動オープン(up だけ。開く番号が起動できたとき) + alt 起動できなかったものがある + C-->>U: 起動できなかったときの報告・終了コード 1 + else 全部が起動できた + C-->>U: 完了の行・終了コード 0 + end +``` + +| 項目 | `up` | `scale` | +| --- | --- | --- | +| 待つ番号 | 1 から台数まで | 1 から新しい台数まで(既存のインスタンスを含む) | +| 後処理の対象 | 起動できたインスタンス | 増やしたインスタンス(前の台数 + 1 から新しい台数まで)のうち起動できたもの | +| エディタの自動オープン | 行う | 行わない | +| 全部が起動できたときの完了の行 | `=== Deploy completed successfully ===` | `=== Scale completed successfully ===` と、増やした番号ごとの `devbase login <番号>` | +| 起動できなかったときの報告の先頭 | `Deploy failed: ...` | `Scale failed: ...` | + +`scale` の既存のインスタンスは、`up` か前の `scale` で処理が済んでいるものとして対象に入れない。 + +| 場合 | 後処理 | 出力 | 終了コード | +| --- | --- | --- | --- | +| 全部が起動できた | 後処理の対象の全部へ行う | 完了の行 | 0 | +| 起動できなかったものがあり、後処理の対象が空でない | 後処理の対象へ行う。エディタは開く番号が起動できたときだけ開く | 続ける旨の警告(処理先として後処理の対象の名前を出す) → 個々の処理の出力 → 報告 | 1 | +| 起動できなかったものがあり、後処理の対象が空である | 行わない | 行わない旨の警告(処理先の名前を出さない) → 報告 | 1 | +| `docker compose up` が非 0 | 行わない | `Deploy failed: ...` / `Failed to start new containers` | 1 | +| 内訳を持たない失敗(構成の生成の失敗など) | 行わない | `Deploy failed: ...` / `Scale failed: ...` | 1 | + +場合は「起動できたものがあるか」ではなく「後処理の対象が空か」で分ける。`scale` では、既存のインスタンスが +起動できていても対象に入らないためである。起動の待ちより後に、`up` / `scale` はコンテナを停止・削除しない。 +起動できなかったインスタンスがあると、本体は後処理へ渡す構成のファイルとして生成物の決まった置き場 +(`.docker-compose.scale.yml`)を使う。 + +出力は次の形で、報告(`Deploy failed` 以降)を最後に置く。利用者は出力の末尾を読むため、`./deploy` の出力に +埋もれないようにする。1 行目は警告、`Deploy failed` 以降はエラーの水準である(`up` でプロジェクト `myapp` の +`dev-2` が終了した例)。 + +```text +起動できなかったインスタンスがあります: dev-2。dev-1 へ起動の後の処理を続けます +…(個々の処理の出力)… +Deploy failed: Container startup failed: 1 of 2 instances did not become ready. + - dev-2: exited unexpectedly + Last logs: + … +起動できなかったインスタンスには、起動の後の処理を行っていません。補う手順: + 落ちたコンテナを起こし直し、その起動が終わった後に、次のコマンドで起動の後の処理をやり直します(./deploy は走りません) + devbase project post-start myapp + docker start で起こし直しただけでは、起動の後の処理(token の配布・窓のタイトルの設定など)は行われません + ./deploy を含めて最初からやり直すには devbase up myapp を打ちます(全インスタンスを作り直します) +``` + +後処理の対象が空のときの 1 行目は次の形である。 + +```text +起動できなかったインスタンスがあります: dev-2。起動の後の処理を行うインスタンスが無いため、起動の後の処理は行いません +``` + +### エディタの自動オープン + +`up` のエディタの自動オープンは、開く番号(`--open-index` か `DEVBASE_OPEN_INDEX`。無ければ 1)のインスタンスが +起動できたときだけ行う。起動できなかったときは +`<開発サービス名>-<番号> は起動できなかったため、エディタを開きません (起こし直した後に devbase open <番号> で開けます)` +を警告で出し、ほかの番号へ替えない。利用者が指定したのと違うインスタンスで作業を始めさせないためである。 + +### やり直しのコマンド(`devbase project post-start`) + +| 項目 | 内容 | +| --- | --- | +| 形 | `devbase project post-start [name] [--context NAME]` | +| 入力 | `name`: 対象のプロジェクト(省略時は実行時のディレクトリ)。`--context`: 接続先(ほかの `project` のサブコマンドと同じ) | +| 処理 | 後処理の対象へ、不足リポジトリの報告 → token の配布 → 窓のタイトルの設定。`./deploy` は走らせず、エディタも開かない | +| 出力 | 個々の処理の出力、処理しなかった番号の警告、完了の行 `=== Post-start completed ===` | +| 前方一致 | `project po` で解決する。`project p`(`ps`)と `project pr`(`profile`)は変わらない | + +後処理の対象は、動いていて entrypoint の完了の印を確かめられたインスタンスである。動いているインスタンスは +`running_dev_instances`(`docker ps` を 1 回)で調べ、その 1 つ 1 つに `docker exec <コンテナ> test -f /tmp/entrypoint-ready` +を 1 回だけ打つ。待たず、繰り返さない。確認の呼び出しそのものが失敗した番号は、完了の印を確かめられない番号として扱う。 +`docker start` で起こし直した直後は entrypoint が走っている最中でリポジトリの clone も終わっていないため、動いている +だけで対象にすると、不足リポジトリを誤って報告し、用意の途中のホームへ token と窓のタイトルを書くことになる。 + +| 状態 | 動き | 終了コード | +| --- | --- | --- | +| 後処理の対象が 1 つ以上ある | それらへ処理を行う。`project.yml` の `scale` までの番号のうち、動いていないもの(`動いていないため処理しません: ...`)と、動いているが完了の印を確かめられないもの(`entrypoint の完了を確かめられないため処理しません: ... (起動が終わってから、もう一度打ちます)`)を警告で出す | 0 | +| 動いているが、完了の印を確かめられたものが 1 つも無い | 上の警告と `起動の後の処理を行えるインスタンスがありません` を出し、処理を行わない | 1 | +| 1 つも動いていない | `動いている <開発サービス名> のインスタンスがありません。起動には devbase up を使います` を出し、起動しない | 1 | +| 状態を取得できない(docker の daemon に届かない) | `dev コンテナの状態を取得できないため、起動の後の処理を行えません` を出す | 1 | +| プロジェクトか接続先を解決できない | ほかの `project` のサブコマンドと同じ | 1 | + +接続先の反映と機密の用意は `devbase project login` と同じ手順(`_prepare_compose`)で行う。ボリュームも構成も作らない +ため、グループの宣言の検査と `project.yml` の書き換えは行わない。後処理へ渡す構成のファイルは、生成物があればそれを +使う。コンテナを起動・停止・削除しない。 + +設計の判断は次のとおりである。 + +- 起動できたインスタンスへその場で後処理を行うことと、やり直しのコマンドの両方を持つ。その場の後処理だけでは、 + 落ちたインスタンスを起こし直した後に補う道が全インスタンスを作り直す `devbase up` だけになる(`scale` の後は + `devbase scale` が同じ数を断る)。やり直しのコマンドだけでは、利用者が打つまで token と窓のタイトルが欠け、 + 起動できたインスタンスの `./deploy` を補えない +- やり直しを `up` のオプションにしないのは、付け忘れ 1 つで動いているインスタンスが作り直されるためである。 + `re` で始まる名前は `project r` / `project re`(`rebuild`)の前方一致を曖昧にするため使わない。トップレベルの + 短縮形・非推奨の `container` グループ・TUI の操作の一覧には置かない +- やり直しは `./deploy` を走らせない。役目を devbase が自分で行う処理(不足リポジトリの報告・token の配布・ + 窓のタイトルの設定)の補いに限る。起動できたインスタンスの `./deploy` は `up` / `scale` の中で済んでいる +- やり直しは完了の印が置かれるまで待たない。起動の途中で止まったインスタンスが 1 つあるだけで、やり直しが + 終わらなくなるためである。起動が終わってから打ち直せば足りる + +## 常に成り立つ条件 + +- 待った番号(1 から台数まで)はどれも、待ちの終わりに「起動できた」か「起動できなかった(理由は 1 つ)」の + どちらか一方に決まる +- あるインスタンスが起動できなかったと決まっても、ほかのインスタンスの待ちを打ち切らない +- 待ちの巡の数は制限(60)を超えない。決まったインスタンスへはその後の巡で問い合わせず、1 巡の docker の + 呼び出しはインスタンスあたり 2 回まで(状態と完了の印)である +- インスタンスごとの処理は、後処理の対象の番号にだけ行う +- インスタンスごとの処理の順は、不足リポジトリの報告 → `./deploy` → token の配布 → 窓のタイトルの設定で、 + `up` と `scale` で同じである。やり直しは同じ順から `./deploy` を除く +- 後処理の対象が空なら、処理を 1 つも行わない +- 起動できなかったインスタンスが 1 つでもあれば、`up` / `scale` は終了コード 1 で終わり、完了の行を出さず、 + 起動できなかった全部の名前・理由と補う手順を出す +- `up` / `scale` は起動の待ちより後に、やり直しはどの時点でも、コンテナを起動・停止・削除しない +- エディタの自動オープンは、開く番号のインスタンスが起動できたときだけ行い、ほかの番号へ替えない +- やり直しは `./deploy` を走らせず、エディタを開かない +- token を書く先は後処理の対象のコンテナに限り、token の値を出力・ログ・`docker exec` の引数・環境変数に載せない + (書き方は [secret-backend.md](secret-backend.md) の `~/.vault-token` の経路に従う) + +## 運用 + +- 1 台が起動できなかったときは、出力の最後の手順に従う。落ちたコンテナを `docker start` で起こし直し、その起動が + 終わった後に `devbase project post-start ` を打つ。`./deploy` も含めてやり直すときは `devbase up ` を打つ + (全インスタンスを作り直す) +- 起こし直したインスタンスの `./deploy` を補う道は `devbase up` の打ち直しだけである +- `scale` が起動の後に失敗しても、`project.yml` の `scale` は書き換わったまま残る + ([compose-profiles.md](compose-profiles.md))。同じ数の `devbase scale` は断られるため、補うにはやり直しの + コマンドを使う +- 起動できたと決まった後に終了したインスタンスは、個々の処理がそれぞれの扱い(警告か無言)で飛ばし、終了コードへ + 反映しない + +## テスト観点 + +自動のテストは docker と `subprocess` を差し替え、実物の docker と実物の `DEVBASE_ROOT` に触れない。 + +- 起動の待ちが、最初の巡で終了した 2 台の名前・理由・ログの末尾を内訳と文言に入れること、制限まで完了の印が無い + 番号を時間切れとして入れること、待った番号が起動できた側とできなかった側のどちらか一方にだけ現れること、 + 番号の小さい 1 台が終了しても後から完了した番号を起動できた側へ入れること、1 巡の docker の呼び出しの回数と + 巡の上限を満たすこと(`tests/utils/test_wait_for_containers.py`) +- `scale`(1 から 3)が `dev-2`・`dev-3` にだけ、実コンテナ名で始まる窓のタイトルを設定し、`DEVBASE_WINDOW_TITLE=off` + で設定しないこと、増やしたインスタンスの不足リポジトリを `up` と同じ文言で報告し、揃っていれば何も出さないこと、 + 処理の順が `up` と同じであること(`tests/commands/test_container_post_start.py`・`tests/commands/test_container_scale_order.py`) +- 1 台が起動できなかった `up` / `scale`(`up` の `dev-2`、`scale` の増やした `dev-3`、`scale` の既存の `dev-1`)が、 + 起動できた後処理の対象へ 4 つの処理を行い、1 で終わり、`Deploy failed` / `Scale failed` と全部の名前、 + `devbase project post-start <プロジェクト名>` だけの行、`docker start` では行われない旨の文を出し、完了の行を + 出さないこと。1 行目の処理先の名前が後処理の対象と一致し、対象が空なら処理を 1 つも呼ばないこと。起動の待ちより + 後に `down`・`stop`・`rm` を呼ばないこと(`tests/commands/test_container_post_start.py`) +- 開く番号のインスタンスが起動できなかったときにエディタを開かず警告し、ほかの番号で開かないこと。開く番号が起動 + できていれば、ほかの番号が落ちていても開くこと(`tests/commands/test_container_post_start.py`) +- やり直しが、完了の印のある動いているインスタンスにだけ処理を行い、動いていない番号と完了の印の無い番号を分けて + 出し、`./deploy` を走らせず、エディタを開かず、コンテナを起動しないこと。対象が空・1 つも動いていない・状態を + 取得できないときに 1 で終わること。2 回続けても書き込みの形が変わらないこと。token の書き込み先が対象の + コンテナだけで、値が出力に現れないこと(`tests/commands/test_container_post_start.py`) +- `devbase project post-start [name] [--context NAME]` が解釈され、`project po` が展開され、ラッパーの `[name]` の一覧と + bash / zsh の補完に現れ、トップレベルと `container` グループには無いこと(`tests/cli/test_post_start_command.py`) +- entrypoint が起動のはじめに完了の印を消すこと(`tests/containers/test_entrypoint_ready_marker.py`) +- 実物の docker では、使い捨てのプロジェクト(台数 2)で、印を置いた番号のインスタンスだけを完了の印より前に終了させて + `devbase up` を打ち、出力の手順に従ってやり直しのコマンドまで通す。前後で、起動できたインスタンスのコンテナの ID と + 起動した時刻が変わらないことを見る。`DEVBASE_ROOT` を一時ディレクトリへ向け、確かめた後にコンテナとボリュームを消す +- 窓のタイトルが VS Code に描かれることは人が見る(設定ファイルに値が書かれることまでを機械が確かめる) + +## 関連リンク + +- [用語集: 起動の後の処理(`post-start`)](../glossary.md#起動の後の処理post-start) +- [付随サービス群の後からの起動・停止(compose-profiles.md)](compose-profiles.md)(`scale` の段の順と `./deploy` のフックの環境変数) +- [機密ストアの保存先の差し替え(secret-backend.md)](secret-backend.md)(`~/.vault-token` を書く経路) +- [CLI リファレンス: project](../user/cli-reference/02-project.md)(`up`・`scale`・`post-start` の節) +- [同時起動でもリンクの段が失敗しない張り方(container-link-stage.md)](container-link-stage.md)(1 台が落ちる原因の 1 つだった同時起動の競合) diff --git a/docs/specifications/secret-backend.md b/docs/specifications/secret-backend.md index eed697b2..392bda3e 100644 --- a/docs/specifications/secret-backend.md +++ b/docs/specifications/secret-backend.md @@ -90,9 +90,11 @@ Infisical で個人単位の機密を守るには利用者ごとに project を | 機密の合成 | `lib/devbase/env/runtime.py` | 4 層の機密を対象のプロジェクトのグループで重ねてコンテナへ渡す。読み取りの直後に `DEVBASE_ACCOUNT_GROUP` を外して警告する(`_without_account_group` / `_warned_account_group_refs`)。`SecretStore` をライフサイクル操作 1 回の間持ち回る(`store_for` / `release_store`) | | dispatch 前の注入 | `lib/devbase/cli.py` | `_load_secret_env`。注入を行わないコマンドと、`version: 2` で注入に使うプロジェクトを決める。`--group` / `--layout` / `--group-alias` / `--exclude-project` の引数 | | 同期済みハッシュの控え | `lib/devbase/env/sources.py` | `SourcesManager` と `sources_path`。`version: 2` では置き場のグループごとに控えを分ける。AWS の選んだ取り込み(`aws_profiles`)のハッシュは切り出した中身から求める | -| 取り込みの方針 | `lib/devbase/env/host_import.py` | `secrets/host-import.yml` の読み込みと検査(`load`・`HostImportConfigError`)、対象のグループの方針の決定(`resolve`。端末でない `ask` は `skip` へ落とす)、取り込みの場 `HostImport`(番号の選択 `choose`・y/N の `confirm`・選択の記録 `record`) | +| 取り込みの方針 | `lib/devbase/env/host_import.py` | `secrets/host-import.yml` の読み込みと検査(`load`・`HostImportConfigError`)、対象のグループの方針の決定(`resolve`。端末でない `ask` は `skip` へ落とす)、取り込みの場 `HostImport`(番号の選択 `choose`・y/N の `confirm`・選択の記録 `record`・方針のために飛ばしたことの知らせ `notify_skipped`) | | AWS のプロファイルの切り出し | `lib/devbase/env/aws_profiles.py` | `~/.aws/config` の節の読み取り、連なりの解決と含めた理由、選んだ節だけの tar(時刻を固定)、切り出した中身のハッシュ、`AWS_CONFIG_BASE64` の値に入っているプロファイルの読み取り。出力も終了コードも持たない | | collector の契約 | `lib/devbase/env/collector.py` | `Collector.host_import` でホストのファイルを読むことを宣言する。`source_files` があるのに宣言しない定義は生成時に `ValueError` で、登録簿は警告を出して飛ばす。宣言した collector は `collect_fn(env_file, *, host)` で呼ばれ、`host` に既定の値は無い | +| AWS の collector | `lib/devbase/env/collectors/aws.py` | 認証方法ごとの収集。方法 `2`・`3` の `AWS_DEFAULT_REGION` は region の出所の判定(`_collect_region`)の 1 か所で決める。方法 `3` は鍵の確認の結果を `KeyDecision`(`ACCEPTED` / `DECLINED` / `NOT_ASKED`)にして渡し、参照の region を残すか(`keep_existing`)は呼び元が渡す(`3` は残す、`2` は決め直す)。節の名前と region は `AWSConfigParser.find_profile_region` で読む | +| GCP の collector | `lib/devbase/env/collectors/google.py` | 鍵ファイルの発見(`find_credential_files`。JSON を読まず、何も出力しない)と、その結果 `CredentialFiles`(名前 → 鍵ファイル・正規化で変わった名前・衝突で外した鍵ファイル。`lookup` で名前から鍵ファイルを引く)。正規化と衝突の警告は、取り込むと決めた名前についてだけ `_warn_for_chosen` が出す。`env sync` と控えの更新(`commands/env.py`)も同じ発見の `lookup` で鍵ファイルを引く | | コンテナへの token の配送 | `lib/devbase/env/container_token.py` | 受け取った token を `docker exec` の stdin で各コンテナの `~/.vault-token` へ書く。token の取得と届け先の解決は持たない | | `up` / `scale` の前処理と後処理 | `lib/devbase/commands/container.py` | backend を問わず、起動の先頭でグループの宣言を読み、ボリュームのグループ(プロセスの環境変数)と揃える(`_require_group_declaration`)。`_ensure_env_files` の子プロセスの `env init` へグループを渡す。backend が `openbao` のとき dev サービスへ `BAO_ADDR` を足し、起動後に起動の後の処理の段(`_run_post_start`)で後処理の対象へ token を書く。`devbase project post-start` も同じ段で token を書き直す | | base イメージ | `containers/base/Dockerfile` | OpenBao CLI `bao` を `checksums.txt` で検証して `/usr/local/bin` へ置く | @@ -739,10 +741,16 @@ AWS は取り込みの選択の範囲だけで比べて入れ直す。控えの 見ずに、上の規則で決め直す(前の認証方法の region を新しいプロファイルへ持ち越さない) - AWS `2`・`3` の `skip` の知らせ(`AWS認証: 取り込まない設定のため飛ばしました`)は 1 回の選択で 1 行である。 `3` で鍵の確認が知らせたときは region のために足さない +- ホストの region は、機密の値でなくてもホストの資格情報ファイルから読む値のため、取り込みに数える。`ask` で + 鍵の確認を出さない経路(`3` でホストに鍵が無い・`2`)は y/N を足さず、値と出所の節を入力の既定の値として見せ、 + 利用者が見たうえで Enter を押すことを確認とする。`3` で鍵の確認に答えたときはその答えを region にも当て、 + 断った利用者に断ったプロファイルの region を既定の値として見せない - GCP の鍵ファイル名の拡張子を除いた部分は、`[A-Za-z0-9_]` 以外を `_` へ置き換えてプロファイル名にする (プロファイル名の正規化)。ファイル名の昇順に読み、正規化した名前が衝突したら先のファイルを採る。 `プロファイル名 '<元>' を '<名前>' に正規化しました` と衝突の警告は、取り込むと決めた名前についてだけ、 アクティブプロファイルを尋ねる前に 1 回出す。`env sync` と控えの更新は同じ規則で鍵ファイルを引き、警告を出さない +- 鍵ファイルの発見はプロジェクト ID を読まない。プロジェクト ID は GCP の collector が候補を表示するときに読む + (発見を `env sync` でも使うため、読めない JSON の警告を `sync` へ持ち込まない) `env sync` はソースファイルのハッシュを控えに記録して変更を検出する。`version: 2` では控えを 置き場のグループごとに `$DEVBASE_ROOT/.env.sources..yml` へ分け、それ以外は @@ -1186,6 +1194,11 @@ flowchart TD - `env sync` は、控えに項目があっても、個人共通とチーム共通のどちらにも無い資格情報のキーを書かない - `env init` はホストの資格情報を、`import` と名指ししたグループ以外では利用者が選んだものだけ書く。 collector はグループ名を比べず、方針は `init` から必須の引数で渡る +- `env init` の AWS `2`・`3` は、方針が `skip`(端末でない `ask` を含む)のときと、`3` の鍵の確認で断ったとき、 + ホストの `~/.aws/config` を region のために読まず、参照へホストの region を書かない +- GCP の鍵ファイルの発見はログも標準出力も出さない。`env init` が登録する鍵ファイルと、`env sync`・控えの更新が + プロファイル名から引く鍵ファイルは、同じ発見の規則(ファイル名の昇順・衝突は先勝ち・`default` は + `~/google_credential.json` も引く)で決まる ## データ・設定 @@ -1504,6 +1517,11 @@ groups: 頃と同じ質問の並びとキーを書くこと、設定の誤りで参照を開く前に 1 で止まること、`sync` が選んだ範囲だけを 比べて参照に無いキーを書かないこと、collector の契約(`tests/commands/test_env_host_import.py`)。 設定の読み込み・番号の読み取り・AWS の節の切り出し(`tests/env/test_host_import.py`) +- AWS `2`・`3` の region の方針ごとの書く値・入力の既定の値と出所の節・知らせの行数、`3` で参照の region を残し + `2` で決め直すこと、`find_profile_region` が返す節の名前(`tests/env/test_aws_region_policy.py`)。鍵ファイルの + 発見が何も出力しないこと・`lookup("default")` の受け皿・警告が取り込むと決めた名前にだけ出ること + (`tests/env/test_gcp_credential_files.py`)。1 回の `env init` での正規化と衝突の警告の回数と、`env sync` が + 警告なしに正規化した名前で鍵ファイルを引くこと(`tests/commands/test_env_host_import.py`) - キーの行の持ち主・適用範囲・グループ、プロジェクトの範囲で共通の行を出さないこと、値を持たないこと、認証 1 回と取得 2 回、 キャッシュへ落ちないこと、ファイル backend の行、プロジェクトごとのキーの数(`tests/commands/test_env_rows.py`) - TUI のキーの一覧と編集の委譲の属性・伏せ字・入力の検査・削除の確認・版の食い違い・`SecretStore` へ diff --git a/issues/issue-224-design-decisions.md b/issues/issue-224-design-decisions.md deleted file mode 100644 index f9a97612..00000000 --- a/issues/issue-224-design-decisions.md +++ /dev/null @@ -1,179 +0,0 @@ -# #224・#371: 決定の記録 - -設計の本体は [issue-224-design.md](issue-224-design.md) にある。この文書はその「決定の記録」の節である。 -決定 1〜5 と決定 10 は、要求の未決 1〜6 への答えで、設計の Pull Request の承認で確定する。 - -## 決定の記録 - -### 決定 1: 起動できたインスタンスへ後処理を行ってから失敗として終え、加えて処理だけをやり直すコマンドを用意する - -#371 が挙げた 2 つの直し方は、補う場面が違う。起動できたインスタンスへその場で後処理を行うと、利用者が何も -打たなくても、動いているインスタンスの token・窓のタイトル・`./deploy` が揃う。やり直しのコマンドは、落ちた -インスタンスを起こし直した後と、時間切れになったインスタンスが後から起動を終えた後に、動いているインスタンスを -作り直さずに処理を補う。両方を採ると、欠けたままになる場面が残らない。 - -| 場面 | その場で後処理を行うだけ | やり直しのコマンドだけ | 両方 | -| --- | --- | --- | --- | -| 1 台が起動できなかった直後の、起動できたインスタンス | 揃う | 利用者がコマンドを打つまで欠けたまま。`./deploy` は補えない | 揃う | -| 落ちたインスタンスを `docker start` で起こし直した後 | 補う道は `devbase up` の打ち直しだけ | コマンドで補える(`./deploy` を除く) | コマンドで補える(`./deploy` を除く) | -| 時間切れの後に起動を終えたインスタンス | 補う道は `devbase up` の打ち直しだけ | コマンドで補える(`./deploy` を除く) | コマンドで補える(`./deploy` を除く) | -| `scale` で増やした 1 台が起動できなかった後 | `devbase scale` は同じ数を断るため、`devbase up` の打ち直しになる | コマンドで補える(`./deploy` を除く) | コマンドで補える(`./deploy` を除く) | -| 公開インタフェース | 増えない | コマンドが 1 つ増える | コマンドが 1 つ増える | - -その場の後処理だけにする形は採らなかった。残る道の `devbase up` の打ち直しは、動いているインスタンスを止めて -作り直すうえ、同時の起動で 1 台が落ちる競合(#357)では同じ競合へもう一度入る。やり直しのコマンドだけにする -形も採らなかった。利用者が打つまで token と窓のタイトルが欠けたままになり、`./deploy` を持つプロジェクトでは、 -起動できたインスタンスの `./deploy` を補う道が `devbase up` の打ち直しだけになる。 - -要求の受け入れ条件は、「案 A」の節の A1〜A6 と「案 B」の節の B1〜B7 が完了の条件になる。B8 は、やり直しの -手段だけを採るときの条件のため当たらない。 - -根拠: Mission / Value 1(MVV 版 1) - -### 決定 2: やり直しは新しいサブコマンド `devbase project post-start [name]` にし、`up` のオプションにしない - -`up` は全インスタンスを止めて作り直すコマンドである。`up` にオプションを足す形は、付け忘れ 1 つで動いている -インスタンスが作り直されるため採らなかった。コンテナに触らずに窓を開き直す `devbase open` と同じく、コンテナに -触らない操作は別のコマンドにする。 - -名前は用語集のコンテキストの識別子(`post-start`)と同じ語にした。`re` で始まる名前(`refresh`・`reapply`)は、 -今は `rebuild` に決まる前方一致(`project r`・`project re`)を曖昧にするため採らなかった。`post-start` は -`project p`(`ps`)と `project pr`(`profile`)の解決を変えない。 - -置き場は `project` グループだけにする。トップレベルの短縮形は日常の操作のためのもので、失敗の後にだけ打つ -コマンドは足さない。`container` グループは非推奨のため、新しいコマンドを足さない。TUI の操作の一覧にも足さない。 - -根拠: Mission / P3(MVV 版 1) - -### 決定 3: やり直しは `./deploy` を走らせず、エディタも開かない - -`./deploy` を走らせるかは、要求の未決 3 で利用者が決める項目で、設計の Pull Request の承認で確定する。この設計は、 -要求の前提 10 と受け入れ条件 B6 のとおり「走らせない」で組んである。決めるための材料は次のとおりである。 - -| 材料 | 内容 | 出どころ | -| --- | --- | --- | -| `./deploy` の約束 | `./deploy` はインスタンスごとに冪等に書かれている(同じインスタンスで走らせ直してよい) | 確定仕様 `docs/specifications/compose-profiles.md`(フックの環境変数の節) | -| 走らせ直す既存の経路 | `devbase project profile up X` は、稼働中の全インスタンスへ `./deploy` を呼び直す(`DEVBASE_ACTIVE_PROFILES=X`)。失敗は終了コード 1 に反映する | 同上 | -| `up` / `scale` の `./deploy` | `DEVBASE_ACTIVE_PROFILES` は空。失敗は警告だけで、終了コードへ反映しない | 同上 | -| やり直しの対象 | 動いていて entrypoint の完了を確かめられたインスタンスの全部(決定 13)。`up` / `scale` で既に `./deploy` が済んだインスタンスを含む | 設計の不変条件 I4 | - -要求の前提 10 は、走らせない理由を「`./deploy` は 2 回走らせて同じ結果になるとは限らない」としている。この理由は、 -確定仕様の約束(冪等)と、`profile up` が既に全インスタンスへ呼び直していることに合わないため、この決定の根拠に -使わない。前提 10 の結論(走らせない)だけを受ける。 - -| 観点 | 走らせない(この設計) | 走らせる | -| --- | --- | --- | -| 起こし直したインスタンスの `./deploy` | 補えない。補う道は `devbase up` の打ち直しだけである(設計の「未確認のまま残ること」) | やり直しで補える | -| 既に `./deploy` が済んだインスタンス | 走らない | もう一度走る。確定仕様の約束(冪等)のとおりなら結果は変わらず、`./deploy` の時間と出力が対象の数だけ増える | -| 終了コード | `./deploy` に左右されない(決定 11) | `./deploy` が失敗したときの扱い(`up` / `scale` と同じ警告だけか、`profile up` と同じ 1 か)を新しく決める | -| 要求と設計の直し | 無い | 要求の前提 10・受け入れ条件 B6、設計の不変条件 I5・I10、テスト設計の B6 の行、出力の「`./deploy` は走りません」の文を直す | - -「走らせない」のまま進める理由は 2 つである。1 つ目は、要求の受け入れ条件(B6)がこの形で書かれていて、採否を -変えるのは利用者の承認だからである。2 つ目は、やり直しの役目を「devbase が自分で行う処理(不足リポジトリの報告・ -token の配布・窓のタイトルの設定)を補うこと」に限ると、起動できたインスタンスの `./deploy` は `up` / `scale` の -中で済んでいて(決定 1)、欠けるのは起こし直したインスタンスの分だけになるからである。その 1 つのために全部の -対象で `./deploy` を走らせ直す形と、番号を選んで走らせるオプションを足す形は、要求に無いため足さない。 - -利用者が承認で「走らせる」を選ぶときは、上の表の「要求と設計の直し」の行を先に行う。 -エディタは `devbase open` で開けるため、やり直しでは開かない。 - -根拠: 根拠なし(MVV 版 1) - -### 決定 4: `scale` の `./deploy` と token の配布の順を、`up` に合わせて入れ替える - -`up` の並び(`./deploy` → token の配布)を基準にすると、`up` の振る舞いが変わらない。変わるのは `scale` で -増やした分の `./deploy` だけで、token が配られた後ではなく前に走るようになる。同じ `./deploy` は `up` では -token の無い状態で走っているため、token があることを前提にできていない。`up` を `scale` の順へ合わせる形は、 -すべての `up` で `./deploy` が見る状態を変えるため採らなかった。 - -根拠: 根拠なし(MVV 版 1) - -### 決定 5: 開く番号のインスタンスが起動できなかったときは、エディタを開かず、ほかの番号へ替えない - -開く番号は利用者が指定している(`--open-index` か `DEVBASE_OPEN_INDEX`。無ければ 1)。起動できた別の番号の -窓を開くと、利用者は指定と違うインスタンスで作業を始めることになる。開かなかったことは警告で伝える。 -起こし直した後は `devbase open` で開ける。 - -根拠: Mission(MVV 版 1) - -### 決定 6: 起動の待ちは引数と成功時の戻り値を変えず、起動できなかったものがあれば内訳を持つ例外を投げる - -例外で返すと、失敗は既定で失敗のままになる。例外を受けない呼び出し元は、今と同じく失敗として終わる。 -内訳を戻り値で返す形は、戻り値を見ない呼び出し元が部分的な失敗を成功として扱えてしまうため採らなかった。 -起動の段(`_run_deploy_pipeline`・`_run_scale_pipeline`)の戻り値と、起動の待ちを差し替えている既存の -テストの組み(6 ファイル)も変えずに済む。 - -根拠: 根拠なし(MVV 版 1) - -### 決定 7: 1 台が起動できなくても残りを制限まで待ち、決まったインスタンスへは問い合わせ直さない - -待ちを 1 台目で打ち切ると、番号の小さい 1 台が落ちたときに、後から起動を終えるインスタンスが起動できたか -どうかが分からず、後処理を届けられない。代わりに、1 台が落ちた `up` / `scale` が終わるまでの時間は、最長で -制限まで延びる(要求の非機能の条件が認めている)。決まったインスタンスへ問い合わせ直さないのは、docker の -呼び出しを増やさないためである。起動できたと決まった後に終了したインスタンスは、個々の処理が今の扱いで飛ばす。 - -根拠: Value 1(MVV 版 1) - -### 決定 8: 制限時間の数え方(巡の回数)を変えない - -要求の前提 4 が、制限時間を変えないと定めている。今の制限は、1 秒の休みを挟む巡を 60 回という数え方で、 -docker の呼び出しにかかる分だけ実際の時間は 60 秒を超える。実際の時間で数える形へ変えると、今は通っている -遅い起動(初回の clone が長い・接続先が遠い)が時間切れになり得るため、この変更では変えない。数え方は -#374 で扱う。 - -根拠: 根拠なし(MVV 版 1) - -### 決定 9: 個々の処理の関数は、対象を受ける引数を足すだけにし、今の呼び出しの形も受ける - -不足リポジトリの報告・token の配布・窓のタイトルの設定は、今は台数(token の配布は開始の番号も)を受ける。 -対象の番号の並びを受けるキーワード引数を足し、渡されなければ今と同じ範囲に行う。これらの関数を直に呼んでいる -既存のテストが書き換えずに通り、個々の処理の振る舞いが変わっていないことの確かめになる。台数と開始の番号の -引数を外して対象だけを受ける形は、直に呼んでいる既存のテストの書き換えが受け入れ条件 23 の範囲を超えるため -採らなかった。 - -根拠: 根拠なし(MVV 版 1) - -### 決定 10: 実物の docker では、印を置いた番号のインスタンスだけを終了させて確かめる - -受け入れ条件 24 は、使い捨てのプロジェクト(台数 2)で確かめる。dev サービスの起動コマンドを包み、番号ごとの -ボリュームに印があるインスタンスだけを、完了の印より前に終了させる。決まった 1 台が必ず落ち、印を消してから -`docker start` で起こし直せるため、出力の手順をやり直しのコマンドまで通せる。同時の起動の競合(#357)に頼る -方法と、待ちの最中に手で `docker kill` を打つ方法は、落ちるかどうかが偶然と打つ時機に左右されるため採らなかった。 -包みの具体の形(印を置くボリュームとパス)は、実装で走らせて決める。 - -`DEVBASE_ROOT` を一時ディレクトリへ向け、利用者のプロジェクトとボリュームに触れない。確かめた後に、使った -コンテナとボリュームを消す。 - -根拠: Value 3 / P3(MVV 版 1) - -### 決定 11: やり直しは、処理しなかった番号があっても 0 で終わり、処理できる番号が 1 つも無ければ 1 で終わる - -やり直しの役目は、後処理の対象(動いていて entrypoint の完了を確かめられたインスタンス。決定 13)へ処理を -行い直すことである。動いていない番号や、完了を確かめられない番号があっても役目は果たせるため 0 で終わり、 -その番号を警告で伝える。対象が空のとき(1 つも動いていない・動いているが完了を確かめられたものが無い)は -何も行えていないため 1 で終わる。 -動いていないときに `devbase up` へ委ねて起動する形(`devbase open` の動き)は、devbase が起動をやり直さない -という要求の前提 8 と受け入れ条件 B4 に合わないため採らなかった。 - -根拠: 根拠なし(MVV 版 1) - -### 決定 12: 起動できなかったときの報告は、後処理の後、出力の最後に出す - -利用者は出力の末尾を読む。起動できなかった名前・理由・補う手順を、個々の処理の出力より後ろに置くと、 -`./deploy` の出力に埋もれない。起動の待ちの直後には、後処理を続けるかどうかの警告を 1 行だけ出す。 - -根拠: 根拠なし(MVV 版 1) - -### 決定 13: やり直しの対象は、動いていて entrypoint の完了の印を確かめられたインスタンスにし、完了を待たない - -`up` / `scale` は、entrypoint の完了を確かめたインスタンスにだけ処理を行う(要求の前提 4)。やり直しも同じ -2 つの印(コンテナの状態と `/tmp/entrypoint-ready`)で対象を決める。`docker start` で起こし直した直後は -entrypoint が走っている最中で、リポジトリの clone は完了の印の直前に行われる。動いているだけで対象にすると、 -clone の途中のリポジトリを不足として報告し、用意の途中のホームへ token と窓のタイトルを書く。 - -完了の印が無い番号は、動いていない番号と同じく警告に出して処理しない。印が置かれるまで待つ形は採らなかった。 -やり直しは何度打ってもよいコマンドで、起動が終わってから打ち直せば足りる。待つと、起動の途中で止まっている -インスタンスが 1 つあるだけで、やり直しが制限時間まで終わらなくなる。完了の印を見ない形(動いていれば対象にする) -は、上の誤った報告と書き込みが起きるため採らなかった。 - -根拠: Mission / C1(MVV 版 1) diff --git a/issues/issue-224-design.md b/issues/issue-224-design.md deleted file mode 100644 index f00265de..00000000 --- a/issues/issue-224-design.md +++ /dev/null @@ -1,549 +0,0 @@ -# #224・#371: 起動の後の処理を 1 つの段にまとめ、起動できなかったインスタンスがあっても届ける - -要求と受け入れ条件は #224 の本文にある(コピーは [issue-224-requirements.md](issue-224-requirements.md)。 -#371 の分もそこにまとめてある)。この文書は「どう作るか」だけを扱う。決定の記録は -[issue-224-design-decisions.md](issue-224-design-decisions.md) に分けた。 - -要求が設計へ委ねた未決には、次の決定で答える。採否は設計の Pull Request の承認で確定する。 - -| 要求の未決 | 設計の答え | 決定 | -| --- | --- | --- | -| 1. 起動できなかったインスタンスがあるときの直し方 | 起動できたインスタンスへ後処理を行ってから失敗として終える。加えて、処理だけをやり直すコマンドを用意する | 決定 1 | -| 2. やり直す手段の形と名前 | 新しいサブコマンド `devbase project post-start [name]` | 決定 2 | -| 3. やり直す手段が `./deploy` を走らせるか | 走らせない(要求の前提 10 のまま。承認で決めるための材料を決定 3 に並べた) | 決定 3 | -| 4. `scale` の `./deploy` と token の配布の順 | `up` に合わせて入れ替える | 決定 4 | -| 5. 開く番号のインスタンスが起動できなかったときのエディタ | 開かず、ほかの番号へ替えない | 決定 5 | -| 6. 実物の docker で 1 台を終了させる方法 | 使い捨てのプロジェクトの起動コマンドを包み、印を置いた番号だけを終了させる | 決定 10 | - -## ドメインモデル - -### コンテキスト - -| コンテキスト | 何の語が 1 つの意味に決まるか | -| --- | --- | -| 起動の後の処理(`post-start`) | 起動の待ち・起動の結果・起動できた / 起動できなかったインスタンス・後処理の対象・インスタンスごとの処理・起動の後の処理のやり直し | - -変更は 1 つのコンテキストで閉じる。ほかの 3 つのコンテキストには順応者として接し、そのモデルを変えない。 - -- エディタで開く(`editor`): 窓のタイトルの設定と、動いているインスタンスの調べ方をそのまま使う -- 機密の置き場(`secret`): token の発行と、コンテナへの書き込みをそのまま使う -- Compose の構成(`compose`): 開発サービス名と生成物(`.docker-compose.scale.yml`)をそのまま使う - -### 集約 - -| 集約 | 持ち主(書き換えてよいもの) | 根 | エンティティ | 値オブジェクト | -| --- | --- | --- | --- | --- | -| 起動の結果 | 起動の待ち(`lib/devbase/utils/docker.py`) | 1 回の待ちの結果 | インスタンス(番号で識別する) | 起動できなかった理由・ログの末尾 | -| 起動の後の処理の実行 | `up` / `scale` の本体・やり直しのコマンドと、起動の後の処理の段(どれも `lib/devbase/commands/container.py`) | 1 回の実行 | — | 後処理の対象(番号の集合)・処理の並び | - -「起動の後の処理の実行」の中の持ち分は次のとおりである。後処理の対象を決めるのは、`up` / `scale` の本体と -やり直しのコマンド(`cmd_up`・`cmd_scale`・`cmd_post_start`)だけである(E6)。起動の後の処理の段(`_run_post_start`)は、 -決まった対象を引数で受け取り、処理の並びをその順に行うだけで、対象を自分で決めず、受け取った対象へ番号を足しも -引きもしない。 - -どちらの集約も 1 回のコマンドの中だけで生き、保存しない。窓のタイトルの設定ファイルと `~/.vault-token` は -`editor` と `secret` の持ち物で、この変更は書き方を変えない。`project.yml` の `scale` の書き換えも変えない。 - -### 不変条件 - -| # | 集約 | 条件 | 破れたときの扱い | -| --- | --- | --- | --- | -| I1 | 起動の結果 | 待った番号(1 から台数まで)はどれも、待ちの終わりに「起動できた」か「起動できなかった(理由は 1 つ)」のどちらか一方に決まる | 決まらない番号が後処理から漏れるか、起動していないものへ処理が走る。テストが落とす | -| I2 | 起動の結果 | あるインスタンスが起動できなかったと決まっても、ほかのインスタンスの待ちを打ち切らない | 番号の小さい 1 台が落ちると、後から起動できるインスタンスへ処理が届かない。テストが落とす | -| I3 | 起動の結果 | 待ちの巡の数は今の制限(60)を超えない。決まったインスタンスへは、その後の巡で問い合わせない(1 巡の docker の呼び出しはインスタンスあたり 2 回まで) | 待ちが延びる、または docker の呼び出しが増える。テストが落とす | -| I4 | 起動の後の処理の実行 | インスタンスごとの処理は、後処理の対象の番号にだけ行う。対象は、`up` では起動できたインスタンス、`scale` では増やしたインスタンスのうち起動できたもの、やり直しでは、動いていて entrypoint の完了の印を確かめられたインスタンスである | 起動できなかったインスタンス・`scale` の既存のインスタンス・entrypoint が終わっていないインスタンスへ処理が走る。テストが落とす | -| I5 | 起動の後の処理の実行 | インスタンスごとの処理の順は、不足リポジトリの報告 → `./deploy` → token の配布 → 窓のタイトルの設定で、`up` と `scale` で同じである。やり直しは同じ順から `./deploy` を除く | 2 つのコマンドの処理が再びずれる。テストが落とす | -| I6 | 起動の後の処理の実行 | 後処理の対象が空なら、処理を 1 つも行わない | 届け先が無いのに token を発行する。テストが落とす | -| I7 | 起動の後の処理の実行 | 起動できなかったインスタンスが 1 つでもあれば、`up` / `scale` は終了コード 1 で終わり、完了の行を出さず、起動できなかった全部の名前・理由と補う手順を出す | 欠けたことが利用者に伝わらない。テストが落とす | -| I8 | 起動の後の処理の実行 | `up` / `scale` は起動の待ちより後に、やり直しはどの時点でも、コンテナを起動・停止・削除しない | 起動できたインスタンスが止まる、または作り直される。テストが落とす | -| I9 | 起動の後の処理の実行 | エディタの自動オープンは、開く番号のインスタンスが起動できたときだけ行い、ほかの番号へ替えない | 利用者が指定していないインスタンスの窓が開く。テストが落とす | -| I10 | 起動の後の処理の実行 | やり直しは `./deploy` を走らせず、エディタを開かない | `./deploy` が済んだインスタンスで、要求(受け入れ条件 B6)に反して `./deploy` がもう一度走る。テストが落とす | -| I11 | 起動の後の処理の実行 | token を書く先は後処理の対象のコンテナに限り、token の値を出力・ログ・`docker exec` の引数・環境変数に載せない | 対象でないコンテナへ token が渡る、または値が漏れる。テストが落とす | - -### ドメインイベント - -| # | イベント | 発生元 | 受け手 | -| --- | --- | --- | --- | -| E1 | コンテナを起動した | `up` / `scale` の本体 | 起動の待ち | -| E2 | 起動の待ちを始めた | 起動の待ち | 起動の待ち(番号ごとの確認) | -| E3 | インスタンスが起動できた | 起動の待ち | 起動の結果(起動できた側へ入れる) | -| E4 | インスタンスが起動できなかった(終了した・見つからない) | 起動の待ち | 起動の結果(理由とログの末尾を入れる)。ほかの番号の待ちは続ける | -| E5 | 制限時間が切れた | 起動の待ち | 起動の結果(決まっていない番号を時間切れとして入れる) | -| E6 | 後処理の対象を決めた | `up` / `scale` の本体・やり直しのコマンド | 起動の後の処理の段 | -| E7 | 不足リポジトリを報告した | 起動の後の処理の段 | 利用者(警告の行) | -| E8 | `./deploy` を走らせた | 起動の後の処理の段 | プロジェクトの `./deploy`(子プロセス) | -| E9 | token を配った | 起動の後の処理の段 | 対象のコンテナの `~/.vault-token` | -| E10 | 窓のタイトルを設定した | 起動の後の処理の段 | 対象のコンテナの VS Code の設定ファイル | -| E11 | エディタを開いた | `up` の本体 | 利用者の VS Code | -| E12 | 結果を報告して終えた | `up` / `scale` の本体 | 利用者(出力と終了コード) | -| E13 | 利用者が、落ちたインスタンスを起こし直した | 利用者(`docker start` など) | やり直しのコマンド(利用者が続けて打つことで受ける。devbase は自分では受けない) | -| E14 | 起動の後の処理をやり直した | やり直しのコマンド | 起動の後の処理の段 | - -### 用語 - -| 用語 | 意味 | 用語集への反映 | -| --- | --- | --- | -| 起動の後の処理 | `up` と `scale` が、起動の待ちの後にホストの側から行う処理の並び | 追加済み(`post-start`、要求の段階) | -| インスタンスごとの処理 | 起動の後の処理のうち、インスタンスの 1 つ 1 つに行う 4 つ | 追加済み(`post-start`、要求の段階) | -| 起動の待ち | 起動したインスタンスの entrypoint の完了を、制限時間まで待つ段 | 追加済み(`post-start`、要求の段階) | -| 起動できたインスタンス | 起動の待ちで、entrypoint の完了を確かめられたインスタンス | 追加済み(`post-start`、要求の段階) | -| 起動できなかったインスタンス | 起動の待ちで、終了した・見つからない・時間切れのどれかに当たったインスタンス | 追加済み(`post-start`、要求の段階) | -| 増やしたインスタンス | `devbase scale` が足した番号のインスタンス | 追加済み(`post-start`、要求の段階) | -| 後処理の対象 | インスタンスごとの処理を行うインスタンスの集合。`up` は起動できたインスタンス、`scale` は増やしたインスタンスのうち起動できたもの、やり直しは動いていて entrypoint の完了の印を確かめられたインスタンス | 意味の変更(`post-start`。やり直しの対象を足す) | -| 起動の結果 | 起動の待ちが決めた、インスタンスごとの「起動できた」「起動できなかった(理由つき)」の内訳 | 追加(`post-start`) | -| 起動できなかった理由 | 起動できなかったインスタンスに 1 つ付く区分。終了した・見つからない・時間切れの 3 つ | 追加(`post-start`) | -| 起動の後の処理の段 | 後処理の対象を受けて、インスタンスごとの処理を決まった順に行う 1 つの関数 | 追加(`post-start`) | -| 起動の後の処理のやり直し | 動いていて entrypoint の完了の印を確かめられたインスタンスへ、インスタンスごとの処理から `./deploy` を除いた 3 つを行い直すコマンド(`devbase project post-start`) | 追加(`post-start`) | - -## 機能一覧 - -| # | 機能 | 誰が使うか | -| --- | --- | --- | -| F1 | `devbase scale` で増やしたインスタンスにも、`up` と同じ起動の後の処理が届く | 開発者 | -| F2 | 1 台が起動できなくても、起動できたインスタンスには起動の後の処理が届く(`up` / `scale`) | 開発者 | -| F3 | 起動できなかったインスタンスの名前・理由と、補う手順を出力で知る | 開発者 | -| F4 | 動いていて entrypoint が終わっているインスタンスへ、コンテナを作り直さずに起動の後の処理をやり直す | 開発者 | -| F5 | `scale` の後処理・起動できなかったときの振る舞い・やり直しのコマンドを文書で調べる | 開発者 | - -## 構成要素 - -| 要素 | 責務 | 新設 / 変更 | 機能 | -| --- | --- | --- | --- | -| 起動の待ち(`lib/devbase/utils/docker.py` の `wait_for_containers_ready`) | 1 から台数までの番号を、巡ごとに状態と完了の印で確かめる。全部が起動できたら戻る。起動できなかった番号があれば、残りを制限まで待った後に、起動の結果を持つ例外を投げる | 変更 | F2・F3 | -| 起動の結果の型(同ファイルの `StartupFailure`・`ContainerStartupError`) | 起動できた番号と、起動できなかった番号ごとの理由・ログの末尾を運ぶ。利用者へ出す文言(名前・理由・ログ)を組み立てる | 新設 | F2・F3 | -| 起動の後の処理の段(`lib/devbase/commands/container.py` の `_run_post_start`) | 後処理の対象を受け、インスタンスごとの処理を決まった順に呼ぶ。対象は呼び出し元が決め、段は決めない。対象が空なら何も呼ばない。`./deploy` を走らせるかを引数で受ける | 新設 | F1・F2・F4 | -| 個々の処理(同ファイルの `_report_missing_repos`・`_push_bao_token`・`_apply_window_titles`) | 対象の番号の並びを受ける引数を足す。渡されなければ今と同じ範囲に行う | 変更 | F1・F2 | -| エディタの自動オープン(同ファイルの `_maybe_open_editor`) | 起動できた番号の並びを受ける引数を足す。開く番号がそこに無ければ、警告して開かない | 変更 | F2 | -| `up` の本体(`cmd_up`) | 起動の待ちの例外を受けて後処理の対象を決め、段とエディタの自動オープンを呼ぶ。起動できなかったものがあれば報告して 1 で終わる | 変更 | F2・F3 | -| `scale` の本体(`cmd_scale`) | 同じく例外を受け、増やしたインスタンスのうち起動できたものを対象に段を呼ぶ。個別に呼んでいた token の配布と `./deploy` を段へ置き換える | 変更 | F1・F2・F3 | -| 起動できなかったときの報告(`_report_startup_failure`) | `Deploy failed` / `Scale failed` の行、起動できなかった番号ごとの名前・理由・ログ、補う手順を出す | 新設 | F3 | -| やり直しのコマンド(`cmd_post_start`) | 接続先と機密を用意し、動いているインスタンスを調べ、その 1 つ 1 つの完了の印(`/tmp/entrypoint-ready`)を確かめて後処理の対象を決め、段を `./deploy` なしで呼ぶ | 新設 | F4 | -| CLI の登録(`lib/devbase/cli.py`・`bin/devbase`・`_dispatch_lifecycle`・`etc/` の補完 2 つ) | `project` グループへ `post-start [name] [--context NAME]` を足す | 変更 | F4 | -| テスト(`tests/utils/`・`tests/commands/`・`tests/cli/`) | 「テスト設計」の行を縛る | 新設・変更 | — | -| 利用者向けの文書と `CHANGELOG.md` | CLI リファレンス(目次、`up`・`scale`・`post-start` の節、`[name]` を取るサブコマンドの一覧)、コンテナ操作ガイドの同じ一覧、環境変数ガイドの「ウィンドウタイトル」の節、`[Unreleased]` の記入 | 変更 | F5 | -| 確定仕様(`docs/specifications/` の `compose-profiles.md`・`cli-argument-resolution.md`・`secret-backend.md`) | `compose-profiles.md` と `cli-argument-resolution.md` は、`scale` の後処理の順序の記述と、`[name]` を取るサブコマンドの一覧を直す。`secret-backend.md` は、`~/.vault-token` を書く経路の記述(token を渡す表の「いつ」の列とその下の箇条書き、構成要素の表の「`up` / `scale` の前処理と後処理」の行、テストの一覧)を直す。書き手に `devbase project post-start` を足し、`up` / `scale` の書き先を後処理の対象(`up` は起動できたインスタンス、`scale` は増やしたインスタンスのうち起動できたもの)に絞る | 変更 | F5 | -| 用語集(`docs/glossary/glossary.json` と `docs/glossary.md`) | 4 語を足し、1 語の意味を変える | 変更(設計の Pull Request で行う) | — | - -起動の待ちが 1 台目で打ち切るために使っていた補助(`check_containers_running`)は、呼び出し元が無くなるため外す -(`grep -rn check_containers_running lib tests` の結果は、`lib/devbase/utils/docker.py` の定義と、起動の待ちからの呼び出しの 2 行)。 - -**`project` グループのサブコマンドの集合へ `post-start` を足すため、その集合を前提にした既存の規則を確かめた。** -当てはまらない規則は次のとおりで、どれも上の表の「CLI の登録」「利用者向けの文書」「確定仕様」に含めた。 - -- 前方一致の展開の一覧(`cli.py` の `SUBCMD_MAP` の `project`)と、`project` の parser の登録 -- `[name]` をラッパーが解決するサブコマンドの一覧(`bin/devbase` の `_PROJECT_NAME_SUBCOMMANDS`) -- 入口の振り分け(`_dispatch_lifecycle` の `handlers`) -- 補完のサブコマンドの一覧(`etc/devbase-completion.bash` の `project_subcommands` と、`etc/_devbase` の `project` の一覧) -- `[name]` を取るサブコマンドを並べた文書(CLI リファレンスの目次と `project` グループの冒頭、コンテナ操作ガイドの冒頭、確定仕様の `compose-profiles.md` と `cli-argument-resolution.md`) - -トップレベルの短縮形(`SHORTCUTS`・`_NAME_RESOLVABLE_SHORTCUTS`)、`container` グループ、TUI の操作の一覧には -足さない(決定 2)。 - -**`~/.vault-token` を書く経路が変わるため、書き手を列挙している確定仕様も確かめた。** -`docs/specifications/secret-backend.md` は、書く経路を「`up` の [5/6] の後(`scale` は増やしたインスタンスだけ)と -`env token`」と列挙している。この変更は書き手に `devbase project post-start` を足し、`up` / `scale` の書き先を -後処理の対象に絞るため、上の表の「確定仕様」の行に含めた。 - -### 文脈 - -```mermaid -graph LR - User[利用者] -->|up・scale・やり直し| Devbase[devbase のコマンド] - Devbase -->|起動・状態・exec| Docker[docker の daemon] - Devbase -->|token の発行| Bao[OpenBao のサーバ] - Devbase -->|窓を開く| Code[利用者の VS Code] -``` - -外部の系は docker の daemon・OpenBao のサーバ・VS Code の 3 つで、どれも変えない。`./deploy` は利用者のプロジェクトが持つスクリプトで、devbase は呼ぶだけである。 - -### 構成要素の関係 - -```mermaid -graph TD - subgraph 入口 - CLI[CLI の登録] - end - subgraph 本体 - Up[up の本体] - Scale[scale の本体] - Redo[やり直しのコマンド] - end - subgraph 共有の段 - Wait[起動の待ち] - Stage[起動の後の処理の段] - Report[起動できなかったときの報告] - end - subgraph 処理 - Each[個々の処理と ./deploy] - Editor[エディタの自動オープン] - end - CLI --> Up - CLI --> Scale - CLI --> Redo - Up --> Wait - Scale --> Wait - Up --> Stage - Scale --> Stage - Redo --> Stage - Up --> Report - Scale --> Report - Stage --> Each - Up --> Editor -``` - -| 呼ばれる要素 | 呼び出し元 | -| --- | --- | -| `up` の本体・`scale` の本体・やり直しのコマンド | CLI の登録(入口の振り分け) | -| 起動の待ち | `up` の本体(起動の段 `_run_deploy_pipeline` を通す)・`scale` の本体(起動の段 `_run_scale_pipeline` を通す) | -| 起動の後の処理の段 | `up` の本体・`scale` の本体・やり直しのコマンド | -| 起動できなかったときの報告 | `up` の本体・`scale` の本体 | -| 個々の処理と `./deploy` | 起動の後の処理の段 | -| エディタの自動オープン | `up` の本体 | - -起動の結果の型は、起動の待ちが作り、`up` / `scale` の本体と報告が読む。テスト・文書・確定仕様・用語集は呼び出しの関係を持たない。 - -### 配置 - -```mermaid -graph TD - subgraph ホスト - CLI[devbase のプロセス] - Deploy[プロジェクトの ./deploy] - end - subgraph "docker の daemon(手元かリモート)" - C1[起動できたインスタンス] - C2[起動できなかったインスタンス] - end - CLI -->|状態・完了の印・ログの末尾| C2 - CLI -->|リポジトリの一覧・token(標準入力)・設定ファイル| C1 - CLI -->|子プロセス・番号は環境変数| Deploy -``` - -配置は変えない。境界をまたいで届く token は、今と同じく `docker exec` の標準入力だけで渡す。 - -### 置き場所 - -```text -lib/devbase/ -├── utils/docker.py # 起動の待ちを変える。起動の結果の型を足す -├── commands/container.py # 段・報告・やり直しのコマンドを足す。up / scale の本体と個々の処理を変える -└── cli.py # project post-start を登録する -bin/devbase # [name] を解決するサブコマンドの一覧 -etc/ -├── devbase-completion.bash # project のサブコマンドと post-start の補完 -└── _devbase # 同上(zsh) -tests/ -├── utils/test_wait_for_containers.py # 新設(起動の待ち) -├── commands/test_container_post_start.py # 新設(up / scale の後処理と報告・やり直し) -├── commands/test_container_scale_order.py # 2 つのテストを書き換える(テスト設計) -└── cli/test_post_start_command.py # 新設(登録・補完・ラッパーの一覧) -docs/ -├── user/ # cli-reference(README.md・02-project.md)・environment-variables.md・container-operations.md -├── specifications/ # compose-profiles.md・cli-argument-resolution.md・secret-backend.md -└── glossary/glossary.json # glossary.md は render で作り直す -CHANGELOG.md -``` - -## 構造 - -```mermaid -classDiagram - class DockerError - class ContainerStartupError { - +ready: tuple~int~ - +failures: tuple~StartupFailure~ - } - class StartupFailure { - +index: int - +service: str - +reason: str - +logs: str - } - DockerError <|-- ContainerStartupError - ContainerStartupError "1" --> "1..*" StartupFailure -``` - -`ready` と `failures` は番号の昇順で、合わせると待った番号の全部になる(I1)。`reason` は起動できなかった理由の -3 つの値(`exited`・`not_found`・`timeout`)のどれかで、`logs` は `exited` のときだけ中身を持つ。 - -| 関数 | 入力 | 出力 | 失敗 | -| --- | --- | --- | --- | -| `wait_for_containers_ready(container_prefix, scale, ready_file, timeout, compose_file)` | 今と同じ | 全部が起動できたら `True` | 起動できなかった番号が 1 つ以上: `ContainerStartupError` | -| `_run_post_start(project_name, scale, indices, dev_service_name, config, compose_file, run_deploy=True)` | 後処理の対象の番号の並び | 無し(出力は個々の処理が出す) | 投げない。個々の処理の失敗は今のまま警告か debug のログにとどまる(前提 5) | -| `_report_missing_repos(...)`・`_push_bao_token(...)`・`_apply_window_titles(...)` | 今の引数に、キーワード引数 `indices`(既定 `None`)を足す | 無し | 今と同じ | -| `_maybe_open_editor(...)` | 今の引数に、キーワード引数 `started`(既定 `None`。起動できた番号の並び)を足す | 無し | 今と同じ | -| `_report_startup_failure(label, error, project_name)` | `Deploy` か `Scale`、起動の待ちの例外、プロジェクト名 | 無し(ログへ出す) | 投げない | -| `cmd_post_start(project_name=None, context=None)` | プロジェクト名と接続先(どちらも省略できる) | 終了コード 0 / 1 | 終了コードで返す(入出力の契約) | - -`up` と `scale` の起動の段(`_run_deploy_pipeline`・`_run_scale_pipeline`)は、戻り値も、起動の待ちを最後に呼ぶことも -変えない。起動の待ちの例外はそのまま通し、`up` / `scale` の本体が受ける。例外を受けた本体は、後処理へ渡す -構成のファイルとして生成物の決まった置き場(`.docker-compose.scale.yml`)を使う。 - -## 入出力の契約 - -### 起動の待ち(関数) - -| 項目 | 内容 | -| --- | --- | -| 名前 | `wait_for_containers_ready` | -| 入力 | 今と同じ(開発サービス名・台数・完了の印のパス・制限・構成のファイル) | -| 出力 | 全部が起動できたら `True`(今と同じ) | -| 失敗の形 | 起動できなかった番号が 1 つ以上あれば `ContainerStartupError`。文言は下の形 | -| 互換性 | 引数と成功時の戻り値は変わらない。例外は `DockerError` の派生で、今の `except DevbaseError` がそのまま受ける。文言は 1 台分から全部の分へ変わる | - -```text -Container startup failed: 2 of 3 instances did not become ready. - - dev-2: exited unexpectedly - Last logs: - <そのコンテナのログの末尾。10 行まで> - - dev-3: timeout (60s) waiting for the entrypoint to complete -``` - -| 起動できなかった理由 | 決まる条件 | 番号の行の文言 | -| --- | --- | --- | -| 終了した(`exited`) | 状態が `exited` か `dead` を含む | `exited unexpectedly` と、続く `Last logs:` とログの末尾 | -| 見つからない(`not_found`) | 状態を取れない | `container not found` | -| 時間切れ(`timeout`) | 巡の数が制限に達した時点で、完了の印を確かめられていない | `timeout (<制限>s) waiting for the entrypoint to complete` | - -### `up` と `scale` の出力と終了コード - -| 場合 | 後処理 | 出力 | 終了コード | -| --- | --- | --- | --- | -| 全部が起動できた | 後処理の対象の全部へ行う | 今と同じ完了の行(`scale` は `devbase login <番号>` の行も) | 0 | -| 起動できなかったものがあり、後処理の対象(I4)が空でない | 後処理の対象へ行う。エディタは I9 に従う | 続ける旨の警告(処理先として後処理の対象の名前を出す) → 個々の処理の出力 → 報告 | 1 | -| 起動できなかったものがあり、後処理の対象が空である | 行わない(I6) | 行わない旨の警告(処理先の名前を出さない) → 報告 | 1 | -| `docker compose up` が非 0(前提 6) | 行わない | 今と同じ | 1 | -| 内訳を持たない失敗(構成の生成の失敗など) | 行わない | 今と同じ `Deploy failed: ...` / `Scale failed: ...` | 1 | - -場合は「起動できたものがあるか」ではなく「後処理の対象が空か」で分ける。`scale` では、起動できたインスタンスと -後処理の対象が一致しないためである(既存のインスタンスは起動できていても対象に入らない)。 - -| 例 | 起動できたインスタンス | 後処理の対象 | 1 行目の警告 | -| --- | --- | --- | --- | -| `up`(台数 2)で `dev-2` が終了した | `dev-1` | `dev-1` | 続ける旨(処理先は `dev-1`) | -| `up`(台数 2)で全部が起動できなかった | 無し | 空 | 行わない旨 | -| `scale`(1 から 3)で増やした `dev-3` が起動できなかった | `dev-1`・`dev-2` | `dev-2` | 続ける旨(処理先は `dev-2` だけ。`dev-1` は出さない) | -| `scale`(1 から 3)で既存の `dev-1` が終了した | `dev-2`・`dev-3` | `dev-2`・`dev-3` | 続ける旨(処理先は `dev-2`・`dev-3`) | -| `scale`(1 から 2)で増やした `dev-2` が起動できなかった | `dev-1` | 空 | 行わない旨(`dev-1` へ続けるとは出さない) | - -出力は次の形になり、報告(`Deploy failed` 以降)を最後に置く(`up` でプロジェクト `myapp` の `dev-2` が終了した例)。 - -```text -起動できなかったインスタンスがあります: dev-2。dev-1 へ起動の後の処理を続けます -…(個々の処理の出力)… -Deploy failed: Container startup failed: 1 of 2 instances did not become ready. - - dev-2: exited unexpectedly - Last logs: - … -起動できなかったインスタンスには、起動の後の処理を行っていません。補う手順: - 落ちたコンテナを起こし直し、その起動が終わった後に、次のコマンドで起動の後の処理をやり直します(./deploy は走りません) - devbase project post-start myapp - docker start で起こし直しただけでは、起動の後の処理(token の配布・窓のタイトルの設定など)は行われません - ./deploy を含めて最初からやり直すには devbase up myapp を打ちます(全インスタンスを作り直します) -``` - -後処理の対象が空のときは、1 行目が次の形になり、個々の処理の出力は無い(`scale`(1 から 2)で `dev-2` が終了した例)。 - -```text -起動できなかったインスタンスがあります: dev-2。起動の後の処理を行うインスタンスが無いため、起動の後の処理は行いません -``` - -- 1 行目は警告、`Deploy failed` 以降はエラーの水準で出す。`scale` では `Deploy failed` が `Scale failed` になる -- 1 行目の処理先に出す名前は、後処理の対象の名前だけである。起動できていても対象でないインスタンス(`scale` の既存の - インスタンス)の名前を、処理先として出さない -- テストが縛るのは、`Deploy failed` / `Scale failed`、起動できなかった全部の名前と理由、コマンドだけの行 - (`devbase project post-start <プロジェクト名>`)、`docker start` では行われない旨の文、1 行目の処理先の名前 - (後処理の対象と一致する。対象が空なら処理先の名前が出ない)の 5 つである -- プロジェクト名は、既存の案内(`devbase project logs <プロジェクト名>`)と同じ値を使う - -### やり直しのコマンド - -| 項目 | 内容 | -| --- | --- | -| 名前 | `devbase project post-start [name] [--context NAME]` | -| 入力 | `name`: 対象のプロジェクト(省略時は実行時のディレクトリ)。`--context`: 接続先(ほかの `project` のサブコマンドと同じ) | -| 出力 | 個々の処理の出力(不足リポジトリの警告・token を書いたコンテナ名)、処理しなかった番号の警告(動いていない番号と、entrypoint の完了を確かめられない番号を、理由を分けて出す)、完了の行(`=== Post-start completed ===`) | -| 失敗の形 | 下の表 | -| 互換性 | 新設。既存のコマンド・オプション・環境変数は変えない。前方一致は `project po` で解決され、`project p`(`ps`)と `project pr`(`profile`)は今のままである | - -| 状態 | 動き | 終了コード | -| --- | --- | --- | -| 後処理の対象(動いていて、完了の印を確かめられたインスタンス)が 1 つ以上ある | それらへ、不足リポジトリの報告 → token の配布 → 窓のタイトルの設定を行う。`project.yml` の `scale` までの番号のうち、動いていないものと、動いているが完了の印を確かめられないものを、理由を分けて警告で出す。後者には、起動が終わってからもう一度打つことを添える | 0 | -| 動いているインスタンスはあるが、完了の印を確かめられたものが 1 つも無い | 処理を行わず、その番号と、起動が終わってからもう一度打つことを出す。完了を待たない | 1 | -| 1 つも動いていない | 処理を行わず、その旨と、起動には `devbase up` を使うことを出す。起動しない | 1 | -| 状態を取得できない(docker の daemon に届かない) | 処理を行わない | 1 | -| プロジェクトか接続先を解決できない | ほかの `project` のサブコマンドと同じ | 1 | - -やり直しの対象は、`up` / `scale` と同じ 2 つの印(コンテナの状態と、コンテナの中の `/tmp/entrypoint-ready`。要求の前提 4)で -決める(決定 13)。`docker start` で起こし直した直後は entrypoint がまだ走っていて、リポジトリの clone も終わっていない。その時点で -処理を行うと、不足リポジトリを誤って報告し、用意の途中のホームへ token と窓のタイトルを書くことになるためである。 - -- 完了の印は、動いているインスタンスごとに 1 回だけ確かめる(`docker exec <コンテナ> test -f /tmp/entrypoint-ready`)。 - 待たず、繰り返さない。対象を決めるための docker の呼び出しは、`docker ps` の 1 回と、動いているインスタンスあたり 1 回になる -- 確認の呼び出しそのものが失敗した番号は、完了の印を確かめられない番号として扱う(処理しない側へ倒す) -- 完了の印を確かめられない番号は、動いていない番号と同じく警告に出し、処理を行わない。終了コードは、後処理の対象が - 1 つ以上あれば 0、空なら 1 である(決定 11) - -接続先の反映と機密の用意は `devbase project login` と同じ手順(`_prepare_compose`)で行う。グループの宣言の検査と -`project.yml` の書き換えは行わない(ボリュームも構成も作らないため)。 - -## 処理の流れ - -### `up` と `scale` - -```mermaid -sequenceDiagram - participant U as 利用者 - participant C as up / scale の本体 - participant W as 起動の待ち - participant P as 起動の後の処理の段 - U->>C: devbase up / devbase scale N - C->>C: 構成の生成と起動(E1) - break docker compose up が非 0 - C-->>U: 今と同じ失敗(後処理なし・終了コード 1) - end - C->>W: 1 から台数までを待つ(E2) - loop 巡(制限まで) - W->>W: 決まっていない番号ごとに状態と完了の印を見る(E3・E4) - end - alt 全部が起動できた - W-->>C: 戻る - else 起動できなかったものがある - W-->>C: 起動の結果を持つ例外(E4・E5) - C-->>U: 警告(名前と、後処理を続けるか) - end - C->>C: 後処理の対象を決める(E6) - C->>P: 対象の番号 - P->>P: 不足リポジトリの報告 → ./deploy → token の配布 → 窓のタイトルの設定(E7〜E10) - C->>C: エディタの自動オープン(up だけ。開く番号が起動できたとき。E11) - alt 起動できなかったものがある - C-->>U: 起動できなかったときの報告・終了コード 1(E12) - else 全部が起動できた - C-->>U: 完了の行・終了コード 0(E12) - end -``` - -2 つのコマンドの違いは次の 3 点だけである。 - -| 項目 | `up` | `scale` | -| --- | --- | --- | -| 待つ番号 | 1 から台数まで | 1 から新しい台数まで(既存のインスタンスを含む) | -| 後処理の対象 | 起動できたインスタンス | 増やしたインスタンスのうち起動できたもの | -| エディタの自動オープン | 行う(I9) | 行わない | - -### 起動の待ちの中のインスタンスの状態 - -```mermaid -stateDiagram-v2 - [*] --> 待っている - 待っている --> 待っている: 動いているが完了の印がまだ無い - 待っている --> 起動できた: 完了の印がある - 待っている --> 起動できなかった: 終了した(状態が exited か dead) - 待っている --> 起動できなかった: 見つからない(状態を取れない) - 待っている --> 起動できなかった: 時間切れ(巡の数が制限に達した) - 起動できた --> [*] - 起動できなかった --> [*] -``` - -「起動できた」「起動できなかった」から「待っている」へ戻る遷移は無い(I3)。起動できたと決まった後に -終了したインスタンスは、個々の処理が今の扱い(警告か無言)で飛ばす(前提 5)。 - -1 巡では、決まっていない番号ごとに、状態の確認(`docker compose ps`)を 1 回、動いていれば完了の確認(`docker compose exec ... test -f`)を 1 回行う。 -終了した番号のログの末尾は、決まったときに 1 回だけ取る。決まっていない番号が残っていれば 1 秒休んで次の巡へ進み、巡の数が制限に達したら終える。 - -### やり直し - -```mermaid -sequenceDiagram - participant U as 利用者 - participant R as やり直しのコマンド - participant D as docker - participant P as 起動の後の処理の段 - U->>D: 落ちたコンテナを起こし直す(E13) - U->>R: devbase project post-start [name] - R->>D: 動いているインスタンスを調べる(docker ps) - alt 状態を取得できない - R-->>U: 理由・終了コード 1 - else 1 つも動いていない - R-->>U: その旨・終了コード 1(起動しない) - else 動いている - R->>D: 動いている番号ごとに完了の印を確かめる(1 回ずつ。待たない) - R->>R: 後処理の対象を決める(動いていて、完了の印を確かめられた番号。E6) - alt 完了の印を確かめられた番号が無い - R-->>U: その番号と、起動が終わってから打ち直すこと・終了コード 1 - else 完了の印を確かめられた番号がある - R->>P: 対象の番号・./deploy なし(E14) - P->>P: 不足リポジトリの報告 → token の配布 → 窓のタイトルの設定 - R-->>U: 処理しなかった番号の警告(あれば)・完了の行・終了コード 0 - end - end -``` - -処理の流れの図に含めない構成要素は、CLI の登録・テスト・利用者向けの文書・確定仕様・用語集である。 -起動の結果の型は「起動の結果を持つ例外」として、起動できなかったときの報告は「起動できなかったときの報告」として現れる。 - -## 非機能の実現方式 - -| 大項目 | 要求の条件 | 実現方式 | 確かめ方 | -| --- | --- | --- | --- | -| 性能・拡張性 | 起動の待ちの制限時間(60 秒)を超えて待たない。全インスタンスが起動できたとき、待ちの 1 巡で docker を呼ぶ回数はインスタンスあたり 2 回(状態の確認と完了の確認)を超えない。1 台が落ちた `up` / `scale` が終わるまでの時間は、残りのインスタンスの完了を待つ分だけ延びる(最長で制限時間まで) | 巡の数の上限を今の 60 のまま変えず、1 台が落ちても延ばさない(決定 8)。決まった番号へは問い合わせ直さず、決まっていない番号だけを、状態 1 回と完了の印 1 回で確かめる(I3) | 関数のテストで、巡ごとの docker の呼び出しを数える。制限に達した巡で終わることを見る | -| 運用・保守性 | 起動できなかったインスタンスごとに、名前と理由(終了した・見つからない・時間切れ)が出力に出る。利用者は出力だけで、どのインスタンスの何が欠けたかと、次に打つコマンドを読み取れる | 起動の結果の型が全部の名前・理由・ログの末尾を文言にし、報告がコマンドだけの行を出す(入出力の契約)。報告は出力の最後に置く(決定 12) | 受け入れ条件 10〜15 のテストと、受け入れ条件 24 の実物の docker での出力 | -| セキュリティ | token を書く先は、後処理の対象(やり直しでは、対象のプロジェクトの動いているインスタンス)に限る。token の値を、出力・ログ・`docker exec` の引数・コンテナの環境変数に載せない(今のまま) | 段が、対象の番号だけを token の配布へ渡す。やり直しは、プロジェクトのラベルと開発サービス名の形で絞った `docker ps` の番号のうち、完了の印を確かめられたものだけを渡す。値の受け渡しは今の書き込み(標準入力)を変えない(I11) | 書き込み先のコンテナ名を見るテストと、出力に値が無いことを見るテスト | - -## テスト設計 - -コマンドの水準のテストは、今の `tests/commands/` と同じく docker と `subprocess` を差し替え、実物の docker と -実物の `DEVBASE_ROOT` に触れない(前提 9)。起動の結果は、起動の待ちを差し替えて例外を投げさせて作る。 - -| 受け入れ条件・不変条件 | どの振る舞いで縛るか | どう壊したら落ちるべきか | -| --- | --- | --- | -| 受け入れ条件 1・5 | `scale` を 1 から 3 にすると、`dev-2` と `dev-3` に、それぞれの実コンテナ名で始まるタイトルが設定される。`DEVBASE_WINDOW_TITLE=off` ではどの番号にも設定されない | `scale` の本体から段の呼び出しを外す。止める値を読まずに設定する | -| 受け入れ条件 2・I4 | `scale` を 1 から 3 にしたとき、`dev-1` を対象にした処理の呼び出しが、4 つの処理のどれにも無い | 段へ 1 から全部の番号を渡す | -| 受け入れ条件 3・4 | 増やしたインスタンスの `/work` にリポジトリが無いと `up` と同じ文言の警告が出る。揃っていれば不足の警告が 1 行も出ない | `scale` から不足リポジトリの報告を外す。既存の番号も報告する | -| 受け入れ条件 6・7・I5 | `scale`(1 から 3)の処理が、不足リポジトリの報告 → `./deploy` → token の配布 → 窓のタイトルの設定の順で `dev-2`・`dev-3` に行われ、`up`(台数 2)が `dev-2` に行う処理の並びと一致する | `scale` の token の配布と `./deploy` の順を今のまま残す。段の中の順を入れ替える | -| 受け入れ条件 8 | 全部が起動できた `scale` は 0 で終わり、完了の行と `devbase login <番号>` の行を出し、エディタを開かない | 完了の行を消す。`scale` がエディタを開く | -| 受け入れ条件 9 | 全部が起動できた `up`(台数 2)は、`dev-1` と `dev-2` に 4 つの処理を今と同じ順で行い、エディタを今と同じ条件で開き、0 と完了の行で終わる | 段が対象の一部を落とす。エディタへ渡す引数を変える | -| 受け入れ条件 10・17・18・I7 | 1 台が起動できなかった(`up` の `dev-2`、`scale` の増やした `dev-3`、`scale` の既存の `dev-1`)とき、1 で終わり、`Deploy failed` / `Scale failed` とその名前が出て、完了の行が出ない | 起動できなかったものがあっても 0 を返す。完了の行を出す。既存の番号の失敗を読み捨てる | -| 受け入れ条件 11・12・13・I1 | 起動の待ちの関数で、最初の巡に `dev-2` と `dev-3` が終了していると、例外の内訳と文言に両方の名前・理由・ログの末尾が入る。制限まで完了の印が無い番号は時間切れとして名前が入る。待った番号は、起動できた側とできなかった側のどちらか一方にだけ現れる | 最初の 1 台で打ち切る。時間切れの番号を内訳に入れない。ログの末尾を付けない | -| 受け入れ条件 A2・I2 | 起動の待ちの関数で、`dev-1` が最初の巡で終了し、`dev-2` が後の巡で完了すると、内訳の起動できた側に `dev-2` が入る | 1 台が終了した時点で例外を投げる | -| I3 | 起動の待ちの関数で、1 巡の docker の呼び出しがインスタンスあたり 2 回以下で、決まった番号がその後の巡で問い合わされず、巡の数が制限を超えない | 決まった番号を毎巡問い合わせ直す。制限を超えて巡を続ける | -| 受け入れ条件 14・A1・A5・A6・I4 | `up`(台数 2、`dev-2` が終了)では `dev-1` に 4 つとも行われ(`./deploy` は `DEVBASE_INSTANCE_INDEX=1` で 1 回)、`dev-2` には 1 つも行われない。`scale`(1 から 3、`dev-3` が起動できない)では `dev-2` にだけ行われる。時間切れのときも、起動できた番号にだけ行われる | 起動できなかったものがあると後処理を全部飛ばす。起動できなかった番号も対象に入れる | -| 受け入れ条件 A4・I6 | 全部が起動できなかったとき、処理が 1 つも呼ばれず、エディタも開かず、1 で終わる。`scale`(1 から 2)で増やした `dev-2` が起動できなかったとき(既存の `dev-1` は起動できている)も、処理が 1 つも呼ばれず、1 で終わる | 対象が空でも token の発行へ進む。`scale` で対象が空のときに既存の番号へ処理を行う | -| 出力の契約(1 行目の処理先の名前) | 1 行目の警告に処理先として出る名前が、後処理の対象と一致する。`up`(台数 2、`dev-2` が終了)では `dev-1`、`scale`(1 から 3、`dev-3` が起動できない)では `dev-2` だけで `dev-1` は出ない。`scale`(1 から 2、`dev-2` が起動できない)では、処理先の名前が出ない | 処理先に、起動できた番号の全部を出す。対象が空でも、続ける旨と既存の番号の名前を出す | -| 受け入れ条件 A3・I9 | 開く番号のインスタンスが起動できなかったとき、エディタを開かず、警告が出て、ほかの番号でも開かない。開く番号が起動できていれば、ほかの番号が落ちていても今と同じ条件で開く | 起動できた別の番号へ替えて開く。警告を出さずに飛ばす | -| 受け入れ条件 15・B7 | 起動できなかったインスタンスがある `up` / `scale` の出力に、`devbase project post-start <プロジェクト名>` だけの行と、`docker start` では起動の後の処理が行われない旨の文がある | 手順の行を消す。コマンドを文の途中へ埋める | -| 受け入れ条件 16・I8 | 起動できなかったものがある `up` / `scale` が、起動の待ちより後に `down`・`stop`・`rm` を含む docker の呼び出しをしない。やり直しは `up`・`start`・`down`・`stop`・`rm` を呼ばない | 失敗の後に片付けの停止を足す。やり直しが起動を試みる | -| 受け入れ条件 B1・B5 | `dev-1` と `dev-2` が動いていて、どちらにも完了の印があるときのやり直しは、両方へ窓のタイトルの設定と token の配布を行い、不足リポジトリを `up` と同じ文言で報告し、0 で終わる。2 回続けても、2 回目に渡るタイトルの値と token の書き込みの形(権限 `0600` での置き換え)が 1 回目と同じである | 対象の番号の一部しか段へ渡さない。2 回目で書き込みの形を変える | -| 受け入れ条件 B2・B4・B6・I10 | やり直しは `./deploy` を走らせず、エディタを開かない。動いているインスタンスが無ければ処理を行わず 1 で終わり、コンテナを起動しない。状態を取得できなければ 1 で終わる | やり直しが `./deploy` を走らせる。動いていないときに `up` へ委ねる | -| I4(やり直しの対象)・前提 4 | 台数 3 で `dev-2` が動いているが完了の印が無いとき、やり直しは `dev-1` と `dev-3` にだけ処理を行い(`dev-2` には token の書き込みも無い)、出力に `dev-2` が完了を確かめられない番号として出て、0 で終わる。動いている全部に完了の印が無ければ、処理を 1 つも行わず、token を発行せず、1 で終わる。完了の印の確認は、動いているインスタンスあたり 1 回である | 完了の印を見ずに、動いている番号の全部を段へ渡す。印の無い番号を出力に出さない。印が置かれるまで待つ | -| 受け入れ条件 B8 | 当たらない(やり直しの手段だけを採るときの条件。決定 1) | — | -| 受け入れ条件 B3 | 台数 3 で `dev-2` が動いていないとき(`dev-1` と `dev-3` は動いていて完了の印がある)、やり直しは `dev-1` と `dev-3` にだけ処理を行い、出力に `dev-2` が出る | 1 から台数までの全部へ行う。動いていない番号を出さない | -| I11 | token の書き込み先のコンテナ名が、後処理の対象の番号の実コンテナ名だけである。出力とログに token の値が現れない | 起動できなかった番号にも書く。token の値をログへ出す | -| やり直しのコマンドの登録(決定 2) | `devbase project post-start [name] [--context NAME]` が解釈され、`project po` が展開され、ラッパーの `[name]` を解決する一覧と bash / zsh の補完に現れる。トップレベルと `container` グループには無い | 前方一致の一覧・ラッパーの一覧・補完のどれかから登録を外す | -| 受け入れ条件 19・20・21 | 文書と `CHANGELOG.md` の差分を、実装の Pull Request のレビューで読む | 自動のテストにしない(文書の文言を固定するテストを置かない) | -| 受け入れ条件 22 | 受け入れ条件 1・3・A1・B1 のテストを、実装より先に書いて失敗を見る(変更の前のコードに当てる) | 変更の前のコードで通るテストは、この行を満たさない | -| 受け入れ条件 23 | 下の表のテストだけを書き換え、`env -u DEVBASE_ROOT uv run --locked pytest tests/ -q` が通る | 表に無い既存のテストが 1 件でも落ちる | -| 受け入れ条件 24 | 使い捨てのプロジェクト(台数 2)と実物の docker で、1 台を起動の待ちの間に終了させて `devbase up` を打ち、出力の手順に従う(決定 10)。前後で、起動できたインスタンスのコンテナの ID と起動した時刻が変わらないことも見る(受け入れ条件 B2) | 人と機械が完了判定で 1 回確かめる | - -書き換える既存のテストは次の 2 つで、どちらも `tests/commands/test_container_scale_order.py` にある。 - -| テスト | 固定している今の形 | 書き換える内容 | -| --- | --- | --- | -| `test_scale_runs_the_steps_in_order` | 起動の待ち → token の配布 → `./deploy` の順 | `./deploy` → token の配布の順にする(受け入れ条件 23 に挙がっている順序の変更) | -| `test_scale_hooks_cover_only_the_new_instances` | token の配布の対象を、開始の番号(`start=2`)で渡す | 対象を番号の並びで渡す形にする。固定する条件(増やした番号だけ)は変えない | - -2 つ目は、受け入れ条件 23 の括弧に挙がっていない。対象の渡し方が変わるために必要になる書き換えで、要求の境界の -「確認してから行う」に当たるため、設計の Pull Request の承認で確かめる。同じファイルの「起動の待ちが失敗したら -`./deploy` を走らせない」のテストは、内訳を持たない失敗として今のまま通るため、書き換えない。 - -## 未確認のまま残ること - -| 項目 | 内容 | -| --- | --- | -| 窓のタイトルが VS Code に描かれること | 設定ファイルへ値が書かれることまでを機械が確かめる。描画は人が見る(#246 の実環境の確認と併せてよい) | -| 制限時間の数え方 | 制限は巡の数(1 秒の休みを 60 回)で数えており、docker の呼び出しにかかる分だけ実際の時間は 60 秒を超える。今も同じで、この変更では変えない(決定 8)。要求の非機能の条件を実際の時間の意味で読むなら、要求へ戻して決め直す。数え方そのものは #374 で扱う | -| 起こし直したインスタンスの `./deploy` | やり直しは `./deploy` を走らせない(決定 3)。そのインスタンスの `./deploy` を補う道は `devbase up` の打ち直しだけである。確定仕様は `./deploy` を冪等としているため、やり直しで走らせる形も取れる。どちらにするかは、利用者が設計の Pull Request の承認で決める(要求の未決 3。材料は決定 3) | -| リモート扱いの接続先でのやり直し | 接続先の扱いは `devbase project login` と同じ手順を通すが、リモートの実機では確かめていない。受け入れ条件 24 は手元の docker で行う | -| TUI からのやり直し | TUI の操作の一覧には足していない。足すかは利用者が決める | -| 前の起動の完了の印が残っているインスタンス | 完了の印(`/tmp/entrypoint-ready`)は entrypoint が最後に置き、起動のはじめには消さない。一度 entrypoint を終えたコンテナを `docker start` / `docker restart` で起こし直した直後は、entrypoint が走り直している間も前の印が残り、やり直しの対象に入る。起動の待ちの間に終了したインスタンス(印が置かれる前に終了した。この変更が補う場面)には当たらない。印を起動のはじめに消すのは entrypoint の変更で、要求の境界の「行わない」に当たるため、この変更では扱わない | -| 起動できたと決まった後に終了したインスタンス | 個々の処理が今の扱い(警告か無言)で飛ばし、終了コードへ反映しない(前提 5)。見せ方は要求の範囲の外である | diff --git a/issues/issue-224-requirements.md b/issues/issue-224-requirements.md deleted file mode 100644 index 9a06c593..00000000 --- a/issues/issue-224-requirements.md +++ /dev/null @@ -1,349 +0,0 @@ -# #224: devbase scale で増やしたインスタンスに _report_missing_repos / _apply_window_titles が行われない - -正は課題の本文(#224)で、この文書はその写しである。`spec-copy.py write` で作り直す。手で直さない。 - -## 依頼(原文) - -この文書は #224 と #371 の 2 つの課題の要求と受け入れ条件をまとめたものである。2 つは直す場所が同じで、1 つの設計・1 本の実装で直す(利用者の決定 2026-10-02)。起票時の本文と、まとめて直すと決めたコメントをそのまま写す。 - -### #224 の本文(`devbase scale` で増やしたインスタンスに `_report_missing_repos` / `_apply_window_titles` が行われない) - -> ## 何を見つけたか -> -> `devbase scale` で増やしたインスタンスに、`devbase up` が行う後処理のうち 2 つが行われない。 -> -> | 後処理 | `cmd_up` | `cmd_scale` | -> | --- | --- | --- | -> | `_report_missing_repos`(clone できなかった repo を伝える) | 行う | **行わない** | -> | `_apply_window_titles`(VS Code のウィンドウタイトルをコンテナ名始まりに固定する) | 行う | **行わない** | -> | `_push_bao_token` | 行う | 行う(`start=current_scale + 1`) | -> | `./deploy` | 行う | 行う(`current_scale + 1 .. new_scale`) | -> -> そのため、`devbase scale 3` で増やした `dev-2` / `dev-3` は、 -> -> - repo の clone に失敗していても何も伝わらない(`devbase up` なら警告が出る) -> - VS Code のウィンドウタイトルが `dev-1` と同じ流儀に揃わない -> -> **実害の観測はまだ無い。** `cmd_up` と `cmd_scale` の後処理の集合が違うことまでが分かっている。 -> -> ## どこで見つけたか -> -> #192(`cmd_scale` の長いメソッドと compose config 読み取りの重複)の設計の過程で、 -> `cmd_up` と `cmd_scale` の段階の対応表を作ったときに見つけた。 -> -> **所在(`main` = `cc71c3c`、v4.0.0 の後)。** PLAN65 / #192 で `cmd_scale` は段階へ分かれているが、 -> **後処理の差は残っている。** -> -> ``` -> $ grep -n "_report_missing_repos\|_apply_window_titles" lib/devbase/commands/container.py -> 1338: _report_missing_repos(...) ← cmd_up だけ -> 1353: _apply_window_titles(...) ← cmd_up だけ -> ``` -> -> `cmd_scale` の後処理は `lib/devbase/commands/container.py:1695-1702` にあり、 -> `_push_bao_token`(`:1695`、`start=current_scale + 1`)と `./deploy`(`:1699-1702`)の 2 つだけである。 -> -> `_apply_window_titles`(`:960`)と `_report_missing_repos`(`:1120`)は `scale` だけを受け、 -> 開始の番号を受けない。そのまま `cmd_scale` から呼ぶと、既存のインスタンスにも走る。 -> -> ## なぜこの変更の範囲外なのか -> -> #192 の受け入れ条件(現在の所在は `issues/old/PLAN65_scale-compose-path.md`)は次の 3 つに限っていた。 -> -> - `devbase scale` の Compose 呼び出しを共通経路へ寄せること(子プロセスの環境と起動の対象) -> - `cmd_scale` の段階を関数へ抽出すること(振る舞いを変えない) -> - `docker compose config --format json` の読み取りを 1 つにすること -> -> 後処理を足すのは **`devbase scale` の振る舞いを増やす**変更で、「振る舞いを変えない」抽出の -> 条件に反する。#192 の対象範囲の「含まない」にも -> 「`cmd_scale` の前提の検査そのものの見直し」と並ぶものとして外していた。 -> -> **#192 は v3.7.0 で閉じているため、この制約はもう無い。** `cmd_scale` が段階へ分かれた -> いまは、`cmd_up` の後処理の段階に対応する位置へ 2 つを足せばよい。2 つの関数には -> 対象のインスタンスを受ける引数を足し、増やしたインスタンス(`current_scale + 1` から `new_scale`)だけに行う。 -> `cmd_up` と `cmd_scale` の後処理を、対象のインスタンスの集合を受ける 1 つの段にまとめる形が合う。 -> 開始の番号ではなく集合を受ければ、#371(起動に成功したコンテナだけに後処理を行う)も同じ段で賄える。 -> **実環境での `devbase scale` の確認は #246 で行う**ため、そこで後処理の有無も併せて見られる。 -> -> ## 直さないと何が起きるか -> -> - `devbase scale` で増やしたインスタンスで repo が足りないとき、利用者は理由の分からない状態に -> 出会う。`devbase up` をやり直すと警告が出るため、そこで初めて分かる -> - VS Code を複数のインスタンスへアタッチしたとき、`up` で作ったものと `scale` で足したものの -> ウィンドウタイトルの流儀が揃わない -> - どちらも `devbase up` をやり直せば解消するため、影響は `scale` の直後の一時的なものに留まる -> -> ## 関連 -> -> - #371(`up` で 1 台でも落ちると、起きているコンテナにも後処理が行われない)。直す場所がこの課題と同じである。起動の後の処理を `cmd_up` と `cmd_scale` で共有し、対象のコンテナの集合を受ける段にする。`scale` は増やしたインスタンスを、#371 は起動に成功したコンテナを、その段へ渡す。段の引数の形を 2 度変えないよう、合わせて着手する -> -> ## 由来 -> -> issue #192 -> -> ## 直す場所 -> -> | 項目 | 内容 | -> | --- | --- | -> | 現れている場所 | lib/devbase/commands/container.py の cmd_scale の後処理(増やしたインスタンス) | -> | 直す場所 | cmd_up と cmd_scale の後処理を、対象のコンテナ(インスタンス)の集合を受ける 1 つの段へ統合する(_report_missing_repos / _apply_window_titles に対象を受ける引数を足す。#371 の部分失敗もこの段を共有する。手: 統合 consolidate_duplication)。実機の確認は #246 と併せられる | - -### #371 の本文(`up` でコンテナが 1 台でも落ちると、起きているコンテナにも起動の後の処理が行われない) - -> ## 何を見つけたか -> -> `devbase project up` は、コンテナの起動を待った後に「起動の後の処理」を順に行う(`lib/devbase/commands/container.py` の `cmd_up`、1333 行付近からの `try`)。 -> -> | 処理 | 関数 | -> | --- | --- | -> | clone できなかったリポジトリの報告 | `_report_missing_repos` | -> | プロジェクトの `deploy` スクリプト | `_run_deploy_script_for_instances` | -> | OpenBao の token の配布 | `_push_bao_token` | -> | VS Code の `window.title` をコンテナ名始まりにする | `_apply_window_titles` | -> | エディタの自動オープン | `_maybe_open_editor` | -> -> 起動の待ちで 1 台でも落ちると、`_run_deploy_pipeline` が `DevbaseError`(`Container startup failed: Container dev-2 exited unexpectedly.`)を投げ、**起きている他のコンテナも含めて、これらの処理がすべて飛ばされる**。落ちたコンテナを `docker start` で起こし直しても、これらの処理はやり直されない。 -> -> - 再現(2026-09-30): scale 2 のプロジェクトを新しく足して `devbase project up` した。#357 の競合で `myapp-dev-2` が `ln: Already exists` で落ち、`Deploy failed` で終わった。`myapp-dev-2` を `docker start` で起こすと 2 台とも動いたが、どちらのコンテナの `~/.vscode-server/data/Machine/settings.json` にも `window.title` が無く、VS Code の窓のタイトルがコンテナ名始まりにならなかった -> - 手で `window_title.apply_to_container` を 2 台へ走らせると書けた(処理そのものは動く) -> -> `devbase scale` も同じ形をしている。`cmd_scale` の `_run_scale_pipeline` も `wait_for_containers_ready` を呼ぶため、足した 1 台が落ちると `DevbaseError` で抜け、`_push_bao_token` と `./deploy`(`container.py` の 1695 行付近から)が飛ぶ。 -> -> ## なぜ困るか -> -> - 利用者から見ると、コンテナは動いているのに、タイトル・token・エディタの自動オープンが黙って欠ける。原因が `up` の途中の失敗だと気づきにくい -> - token の配布(`_push_bao_token`)が飛ぶと、起きているコンテナで機密を読めない -> -> ## 直し方の見込み(決めること) -> -> - 起動の待ちで落ちたコンテナがあっても、**起きているコンテナには**起動の後の処理を行い、そのうえで失敗として終える。`wait_for_containers_ready`(`lib/devbase/utils/docker.py`)は 1 台が落ちると、他のコンテナが ready になるのを待たずに止まる。起きているコンテナへ後処理を行うなら、待ちの側が「落ちた番号」と「ready になった番号」を分けて返す必要がある -> - 落ちたコンテナの名前と、起こし直す方法(もう一度 `devbase project up` を打つ。`docker start` では起動の後の処理が走らない)を出す -> - または、起動の後の処理だけをやり直す手段(コマンドか、`up` が既に起きているコンテナでも処理をやり直すこと)を用意する -> -> ## 関連 -> -> - #357(同じグループのコンテナを同時に起動すると 1 つが落ちる)。#357 が直っても、別の理由で 1 台が落ちたときに同じことが起きる -> - #224(`devbase scale` で増やしたインスタンスに `_report_missing_repos` / `_apply_window_titles` が行われない)。直す場所がこの課題と同じである。起動の後の処理を `cmd_up` と `cmd_scale` で共有し、対象のコンテナの集合を受ける段にする。この課題は起動に成功したコンテナを、#224 は増やしたインスタンスを、その段へ渡す。段の引数の形を 2 度変えないよう、合わせて着手する -> -> ## 由来 -> -> 利用者からの報告(2026-10-01、新しく足したプロジェクトの VS Code の窓のタイトルがコンテナ名始まりにならない) - -### 利用者の決定(#371 のコメント、2026-10-02) - -> 利用者の決定(2026-10-02): この課題は #224 と一緒に、スプリント m7a(マイルストーン 03 の今回の切り出し)で直す。設計は #224 と 1 本にまとめ、直し方の案(起きているコンテナへ後処理を行ってから失敗で終える / 後処理だけをやり直す手段を用意する)は設計 Pull Request の承認で決める。切り出しの Size の和は 15(容量 13 を 2 超える)。 - -## 目的 - -- `devbase up` で起動したインスタンスにも、`devbase scale` で増やしたインスタンスにも、起動できたものには同じ起動の後の処理が届く(#224) -- 1 台が起動できなかったときに、起動できたインスタンスの窓のタイトル・token・エディタが黙って欠けたままにならない。何が欠けたかと、その補い方が `up` / `scale` の出力から分かる(#371) -- 起動の後の処理が誰を対象にするかを、`up` と `scale` が同じ 1 つの決まりで決める。2 つのコマンドの処理の集合が再びずれない - -## 現状(2026-10-02 に調べたこと。`main` = `cc71c3c`) - -原文の指摘を、コードと既存のテストの読み取りで確かめた。実物の docker では確かめていない。 - -| 調べたこと | 結果 | -| --- | --- | -| 起動の後の処理の集合 | `up` は 5 つ(不足リポジトリの報告 → `./deploy` → token の配布 → 窓のタイトルの設定 → エディタの自動オープン)を行う。エディタの自動オープンを除く 4 つは番号 1 から `scale` までの全部に、エディタの自動オープンは 1 つの番号にだけ行う。`scale` は 2 つ(token の配布 → `./deploy`)を増やした番号にだけ行う。2 つのコマンドで集合も順序も違う(`lib/devbase/commands/container.py` の `cmd_up` / `cmd_scale`) | -| 対象の渡し方 | 不足リポジトリの報告と窓のタイトルの設定は台数(`scale`)だけを受け、番号 1 から全部に行う。token の配布は開始の番号、`./deploy` は番号の並びを受ける。4 つで形が揃っていない | -| 起動の待ち | `wait_for_containers_ready`(`lib/devbase/utils/docker.py`)は番号 1 から順に状態を見て、終了した(`exited` / `dead`)か見つからない 1 台に当たった時点で、その 1 台の名前とログの末尾 10 行を載せた例外を投げる。ほかのインスタンスの完了を待たず、2 台以上が落ちていても最初の 1 台しか出ない。制限時間(60 秒)が切れたときの文言は `Timeout (60s) waiting for containers to be ready` で、どのインスタンスかは出ない | -| 待ちが失敗したとき | `up` は `Deploy failed: ...` を出して終了コード 1 で終わり、起動の後の処理 5 つをすべて飛ばす。`scale` は `Scale failed: ...` で 1 で終わり、2 つを飛ばす。どちらも起動できたインスタンスは動いたまま残る。`scale` はこの時点で `project.yml` の `scale` を書き換えてある(`tests/commands/test_container_scale_order.py` が現状として固定している) | -| 落ちたインスタンスを起こし直す道 | `docker start` では起動の後の処理が走らない。`devbase up` を打ち直すと、動いているインスタンスも含めて全部を止めて作り直す。`scale` が失敗した後の `devbase scale <同じ数>` は、`project.yml` が書き換わっているため `New scale (N) is not greater than current scale (N)` で断られる | -| 処理だけをやり直す手段 | token の配布は `devbase env token`(動いている dev のコンテナ全部へ配る)、エディタは `devbase open` がある。不足リポジトリの報告・窓のタイトルの設定・`./deploy` には、コンテナを作り直さずにやり直す手段が無い | -| 窓のタイトルが残る場所 | コンテナの中の `~/.vscode-server/data/Machine/settings.json`。`~/.vscode-server` はプロジェクトと番号ごとのボリュームである。新しいプロジェクトの番号と、`scale` で初めて作る番号はボリュームが空のため、窓のタイトルの設定が行われない限りタイトルが無い(#371 の再現と一致する) | -| 個々の処理の失敗の扱い | どの処理も `up` / `scale` を倒さない。不足リポジトリの報告は問い合わせに失敗すると何も出さない。`./deploy` と token の配布は警告を出す。窓のタイトルの設定は debug のログにだけ出す | - -## 前提 - -- 前提 1: `up` の起動の後の処理の並びは変えない。`scale` はその並びからエディタの自動オープンを除いた 4 つ(インスタンスごとの処理)を同じ順に行う。今の `scale` は token の配布 → `./deploy` の順で、変更の後は `./deploy` → token の配布になる -- 前提 2: `scale` の後処理の対象は、増やしたインスタンスに限る。既存のインスタンスは、`up` か前の `scale` で処理が済んでいるものとして扱う -- 前提 3: `scale` はエディタを開かない(今のまま) -- 前提 4: 起動できたかどうかは、起動の待ちが今使っている 2 つの印(コンテナの状態と、コンテナの中の `/tmp/entrypoint-ready`)で決める。制限時間(60 秒)は変えない -- 前提 5: 起動の後の処理の 1 つ 1 つが失敗したときの扱い(警告か無言で続け、終了コードへ反映しない)は変えない -- 前提 6: `docker compose up` そのものが非 0 で終わったときの扱いは変えない(`up` は `Deploy failed`、`scale` は `Failed to start new containers` を出し、起動の後の処理を行わずに終了コード 1 で終わる) -- 前提 7: 接続先(docker context)の扱いは変えない。起動の後の処理は、`up` / `scale` が解決した接続先で動く -- 前提 8: 起動できなかったインスタンスを、devbase が自動で起こし直すことも、起動をやり直すこともしない -- 前提 9: 自動のテストは、今の `tests/commands/` と同じく、実物の docker と実物の `DEVBASE_ROOT` に触れない。実物の docker での確認は使い捨てのプロジェクトで行い、利用者のプロジェクトとボリュームに触れない -- 前提 10: 案 B の「処理だけをやり直す手段」は `./deploy` を走らせない。`./deploy` はプロジェクトが書くスクリプトで、2 回走らせて同じ結果になるとは限らないためである(未決 3) - -## 対象範囲 - -含む: -- `devbase scale` が、増やしたインスタンスへ不足リポジトリの報告と窓のタイトルの設定を行うこと(#224) -- `up` と `scale` の起動の後の処理を、同じ並びにすること(前提 1) -- 起動できなかったインスタンスがあるときの `up` / `scale` の出力(名前・理由・補い方)と終了コード(#371) -- 起動できなかったインスタンスがあるときに、起動できたインスタンスの起動の後の処理が欠けたままにならない道を用意すること(#371。案 A か案 B か両方かは未決 1) -- 利用者向けの文書(CLI リファレンスの `up` と `scale` の節・環境変数ガイドの「ウィンドウタイトル」の節)と `CHANGELOG.md` の更新 - -含まない: -- 同時起動で 1 台が落ちる原因そのもの(#357) -- 起動できなかったインスタンスを devbase が自動で起こし直す仕組み(前提 8) -- `docker compose up` そのものが失敗したときの後処理(前提 6) -- 起動の後の処理の 1 つ 1 つが失敗したときの見せ方(前提 5。窓のタイトルの設定の失敗が debug のログにしか出ないことを含む) -- 既存のインスタンスへの `scale` の後処理(前提 2) -- `scale` が途中で失敗しても `project.yml` の `scale` が書き換わったまま残ること(`docs/specifications/compose-profiles.md` に書かれた今の振る舞い) -- `devbase project profile up` が走らせる `./deploy`(プロファイルのサービスの経路で、dev のインスタンスの起動を伴わない) -- `devbase scale` の実環境での確認そのもの(#246。この変更の後処理は #246 で併せて見られる) -- 設計の成果物のうち画面の図(変えるのはコマンドの出力の行だけで、画面を持たない) - -## ドメインイベント - -| # | イベント | 引き金 | 失敗したとき | 順序の前提 | -| --- | --- | --- | --- | --- | -| E1 | コンテナを起動した | `devbase up` / `devbase scale`(`docker compose up -d`) | `docker compose up` が非 0 → 今のまま、起動の後の処理を行わずに終了コード 1(前提 6) | — | -| E2 | 起動の待ちを始めた | E1 | — | E1 | -| E3 | インスタンスが起動できた | E2。そのインスタンスの entrypoint の完了を確かめた | — | E2 | -| E4 | インスタンスが起動できなかった(終了した・見つからない) | E2。待ちの間にコンテナが `exited` / `dead` になった、または見つからない | 名前と理由とログの末尾を出す(受け入れ条件 11・12)。**今は、ここで待ちを打ち切るため、ほかのインスタンスの E3 が起きない** → 受け入れ条件 A2 | E2 | -| E5 | 制限時間が切れた | E2 から 60 秒 | 完了を確かめられなかったインスタンスの名前を出す(受け入れ条件 13) | E2 | -| E6 | 後処理の対象を決めた | 起動の待ちの終わり(全インスタンスが E3 か E4 に決まった、または E5) | 対象が 0 個 → 処理を 1 つも行わない(受け入れ条件 A4) | **全インスタンスについて E3 / E4 / E5 のどれかが決まっていると仮定している** → 受け入れ条件 A2・A6 | -| E7 | 不足リポジトリを報告した | E6。対象のインスタンスごと | 問い合わせに失敗 → 何も出さずに続ける(前提 5) | E6。**対象のインスタンスが処理の時点でも動いていると仮定している**(E8〜E10 も同じ)→ 前提 5 の扱いのまま | -| E8 | `./deploy` を走らせた | E7 の後。スクリプトがあるときだけ | 非 0 → 警告して次の番号へ続ける(前提 5) | E7 | -| E9 | token を配った | E8 の後。backend が openbao のときだけ | 取れない・書けない → 警告して続ける(前提 5) | E8(前提 1) | -| E10 | 窓のタイトルを設定した | E9 の後。`DEVBASE_WINDOW_TITLE` で止めていないときだけ | 書けない → debug のログに出して続ける(前提 5) | E9 | -| E11 | エディタを開いた | E10 の後。`up` で、自動オープンが有効なときだけ | 開けない → 警告して続ける(前提 5)。開く番号のインスタンスが起動できなかった → 受け入れ条件 A3 | E10 | -| E12 | 結果を報告して終えた | E11 の後(`scale` は E10 の後) | 起動できなかったインスタンスがある → 名前・理由・補い方を出して終了コード 1(受け入れ条件 10〜18) | E6 | -| E13 | 利用者が、落ちたインスタンスを起こし直した | 利用者の操作(`docker start` など)。**devbase の外で起きるため、起動の後の処理の引き金にならない** → 受け入れ条件 15・案 B | — | E12 | -| E14 | 起動の後の処理をやり直した(案 B) | 利用者が「処理だけをやり直す手段」を実行した | 動いているインスタンスが無い → 何も行わず非 0(受け入れ条件 B4) | E12。E13 の前でも後でもよい | - -## 用語 - -| 用語 | 意味 | -| --- | --- | -| 起動の後の処理 | `up` と `scale` が、起動の待ちの後にホストの側から行う処理の並び。不足リポジトリの報告・`./deploy` の実行・token の配布・窓のタイトルの設定・エディタの自動オープンの 5 つ | -| インスタンスごとの処理 | 起動の後の処理のうち、インスタンスの 1 つ 1 つに行う 4 つ(エディタの自動オープンを除いたもの) | -| 起動の待ち | `up` と `scale` が、起動したインスタンスの entrypoint の完了を、制限時間まで待つ段 | -| 起動できたインスタンス | 起動の待ちで、entrypoint の完了を確かめられたインスタンス | -| 起動できなかったインスタンス | 起動の待ちの間に終了した・見つからない・制限時間までに entrypoint の完了を確かめられなかった、のどれかに当たるインスタンス | -| 増やしたインスタンス | `devbase scale` が足した番号のインスタンス(前の `scale` の値 + 1 から、新しい `scale` の値まで) | -| 後処理の対象 | インスタンスごとの処理を行うインスタンスの集合。`up` は起動できたインスタンス、`scale` は増やしたインスタンスのうち起動できたもの | -| 動いているインスタンス | 用語集のとおり(`docker ps` に現れる dev のインスタンス) | -| 開発サービス名 | 用語集のとおり(既定は `dev`。インスタンスは `<開発サービス名>-<番号>`) | -| 窓 | 用語集のとおり(dev のコンテナへ接続した VS Code のウィンドウ) | - -## 受け入れ条件 - -読み方の決まり: - -- 例は開発サービス名が `dev` のプロジェクトで書く。インスタンスは `dev-1`・`dev-2`・`dev-3` と呼ぶ -- 「インスタンスに処理が行われる」は、自動のテストでは、その処理がそのインスタンスの番号(またはコンテナ名)を対象に呼ばれることで確かめる。実物の docker では、コンテナの中の `~/.vscode-server/data/Machine/settings.json` の `window.title`・`~/.vault-token`・コマンドの出力で確かめる -- 案 A・案 B の節は、未決 1 で採ると決めた案の分だけが完了の条件になる。採らなかった案の条件は、消さずに取り消し線で残す - -### #224 の分(`scale` で増やしたインスタンスの後処理) - -- [ ] 1. 前提: `scale` が 1 で動いているプロジェクト - 操作: `devbase scale 3` - 結果: `dev-2` と `dev-3` のそれぞれに窓のタイトルの設定が行われ、設定されるタイトルはそのインスタンスの実コンテナ名で始まる -- [ ] 2. 受け入れ条件 1 の操作で、`dev-1` にはインスタンスごとの処理が 1 つも行われない -- [ ] 3. 前提: `project.yml` のリポジトリの 1 つが `dev-3` の `/work` に無い - 操作: `devbase scale 3`(1 から) - 結果: `devbase up` と同じ文言の警告(`Repositories missing in /work of dev-3 (clone may have failed):` と、そのリポジトリのディレクトリ名と URL の行)が出る -- [ ] 4. 増やしたインスタンスの `/work` にリポジトリが揃っているとき、`devbase scale` は不足の警告を 1 行も出さない -- [ ] 5. `DEVBASE_WINDOW_TITLE=off` で `devbase scale 3` を打つと、どのインスタンスにも窓のタイトルの設定が行われない(`up` と同じ止め方が効く) -- [ ] 6. `devbase scale 3`(1 から)は、`dev-2` と `dev-3` を対象に、不足リポジトリの報告 → `./deploy` → token の配布 → 窓のタイトルの設定の順で行う -- [ ] 7. `devbase up`(`scale` 2)が `dev-2` に行うインスタンスごとの処理と、`devbase scale 2`(1 から)が `dev-2` に行うインスタンスごとの処理が、同じ処理・同じ順である -- [ ] 8. 全インスタンスが起動できたときの `devbase scale` は、今と同じく終了コード 0 で終わり、`=== Scale completed successfully ===` と、増やした番号ごとの `devbase login <番号>` の行を出す。エディタは開かない - -### #371 の分(起動できなかったインスタンスがあるとき) - -- [ ] 9. 全インスタンスが起動できたときの `devbase up`(`scale` 2)は、`dev-1` と `dev-2` にインスタンスごとの処理を今と同じ順で行い、エディタの自動オープンを今と同じ条件で行い、終了コード 0 で `=== Deploy completed successfully ===` を出す -- [ ] 10. 前提: `scale` 2 の `devbase up` で、`dev-2` が起動の待ちの間に終了する - 結果: 終了コード 1 で終わり、`Deploy failed` を含む行が出る。`=== Deploy completed successfully ===` は出ない -- [ ] 11. 受け入れ条件 10 のとき、出力に `dev-2` の名前と、起動できなかった理由(終了した)と、そのコンテナのログの末尾(今と同じ 10 行まで)が出る -- [ ] 12. 前提: `scale` 3 の `devbase up` で、起動の待ちが最初に状態を見た時点で `dev-2` と `dev-3` の両方が終了している - 結果: 出力に `dev-2` と `dev-3` の両方の名前が出る(今は `dev-2` だけ) -- [ ] 13. 制限時間までに entrypoint の完了を確かめられないインスタンスがあるとき、終了コード 1 で終わり、出力にそのインスタンスの名前と理由(時間切れ)が出る(今は名前が出ない) -- [ ] 14. 起動できなかったインスタンスには、インスタンスごとの処理が 1 つも行われない -- [ ] 15. 起動できなかったインスタンスがあるとき、出力に、欠けた起動の後の処理を補う手順(打つコマンド)が出る。手順の文は、`docker start` で起こし直しただけでは起動の後の処理が行われないことを含む -- [ ] 16. 起動できなかったインスタンスがあっても、`up` / `scale` は起動の待ちより後に `down` / `stop` / `rm` を呼ばない(起動できたインスタンスを止めず、消さない) -- [ ] 17. 前提: `devbase scale 3`(1 から)で、`dev-3` が起動の待ちの間に終了する - 結果: 終了コード 1 で終わり、`Scale failed` を含む行と `dev-3` の名前が出る。`=== Scale completed successfully ===` は出ない -- [ ] 18. `devbase scale` で、既存のインスタンス(`dev-1`)が起動できなかったときも、終了コード 1 で終わり、出力に `dev-1` の名前が出る - -### #371 の分・案 A(起動できたインスタンスへ後処理を行ってから、失敗として終える) - -- [ ] A1. 前提: `scale` 2 の `devbase up` で、`dev-2` が起動の待ちの間に終了し、`dev-1` は起動できた - 結果: `dev-1` にインスタンスごとの処理が 4 つとも行われる(`./deploy` は `DEVBASE_INSTANCE_INDEX=1` で 1 回だけ走る)。終了コードは 1 のままである -- [ ] A2. 前提: `scale` 2 の `devbase up` で、番号の小さい `dev-1` が起動の待ちの間に終了し、`dev-2` はその後、制限時間の内に entrypoint が完了する - 結果: `dev-2` にインスタンスごとの処理が 4 つとも行われる(1 台が落ちても、残りの完了を制限時間まで待つ) -- [ ] A3. 起動できなかったインスタンスがあるときのエディタの自動オープンは、開く番号のインスタンスが起動できていれば、今と同じ条件で開く。開く番号のインスタンスが起動できなかったときは開かず、開かなかったことを警告で出し、ほかの番号へ替えて開かない -- [ ] A4. 全インスタンスが起動できなかったときは、インスタンスごとの処理を 1 つも行わず、エディタも開かず、終了コード 1 で終わる -- [ ] A5. 前提: `devbase scale 3`(1 から)で、`dev-3` が起動できず、`dev-2` は起動できた - 結果: `dev-2` にインスタンスごとの処理が 4 つとも行われ、`dev-1` と `dev-3` には 1 つも行われない -- [ ] A6. 制限時間が切れたとき、それまでに起動できたインスタンスには、インスタンスごとの処理が 4 つとも行われる - -### #371 の分・案 B(処理だけをやり直す手段を用意する) - -- [ ] B1. 前提: `dev-1` と `dev-2` が動いていて(`dev-2` は落ちた後に `docker start` で起こした)、どちらにも `window.title` が無い - 操作: 処理だけをやり直す手段を 1 回実行する - 結果: 両方に窓のタイトルが設定され、backend が openbao なら両方に token が配られ、`/work` に無いリポジトリがあれば `up` と同じ文言で報告される。終了コードは 0 -- [ ] B2. 受け入れ条件 B1 の実行の前後で、`dev-1` と `dev-2` のコンテナの ID と起動した時刻が変わらない(止めない・作り直さない) -- [ ] B3. 番号の途中に動いていないインスタンスがあるとき、手段は動いているインスタンスにだけ処理を行い、動いていない番号を出力に出す -- [ ] B4. 動いているインスタンスが 1 つも無いとき、手段は処理を 1 つも行わず、その旨を出して非 0 で終わる。コンテナを起動しない -- [ ] B5. 手段を 2 回続けて実行すると、2 回目の後の `window.title` の値と `~/.vault-token` の権限(`0600`)が、1 回目の後と同じである -- [ ] B6. 手段は `./deploy` を走らせない(前提 10) -- [ ] B7. 受け入れ条件 15 の手順が、この手段のコマンドを 1 行で示す -- [ ] B8. 案 B だけを採るとき、起動できなかったインスタンスがある `up` / `scale` は、今と同じく起動の後の処理を行わずに終わる(受け入れ条件 10〜18 は満たす) - -### 文書とテスト - -- [ ] 19. CLI リファレンスの `scale` の節に、増やしたインスタンスへ行う起動の後の処理が書かれている。`up` の節に、起動できなかったインスタンスがあるときの振る舞い(終了コード・出力・補い方)が書かれている -- [ ] 20. 環境変数ガイドの「ウィンドウタイトル」の節が、`scale` で増やしたインスタンスにもタイトルを設定することを書いている(今は「`up` のたびに」とだけ書いている) -- [ ] 21. `CHANGELOG.md` の `[Unreleased]` の `Fixed` に、利用者に見える 2 つの変更(`scale` の後処理・起動できなかったインスタンスがあるときの扱い)が書かれている -- [ ] 22. 受け入れ条件 1・3 のテストと、採った案の先頭の条件(A1 か B1)のテストは、変更の前のコード(`main` = `cc71c3c`)に当てると失敗する -- [ ] 23. 既存のテストのうち書き換えるのは、この変更で振る舞いが変わる現状固定の条件(`scale` の token の配布と `./deploy` の順序、案 A を採るときの「起動の待ちが失敗したら `./deploy` を走らせない」)を固定しているものだけである。ほかの既存のテストは書き換えずに通る -- [ ] 24. 前提: 使い捨てのプロジェクト(`scale` 2)と実物の docker。1 台が起動の待ちの間に終了するようにしてある - 操作: `devbase up` を打ち、出力に出た手順に従う - 結果: 受け入れ条件 10・11・15 のとおりに出力され、手順を終えた後は、動いているインスタンスのすべてに `window.title` がある。利用者のプロジェクトとボリュームには触れない - -## 非機能の条件 - -| 大項目 | 条件 | -| --- | --- | -| 性能・拡張性 | 起動の待ちの制限時間(60 秒)を超えて待たない。全インスタンスが起動できたとき、待ちの 1 巡で docker を呼ぶ回数はインスタンスあたり 2 回(状態の確認と完了の確認)を超えない。案 A を採るとき、1 台が落ちた `up` / `scale` が終わるまでの時間は、残りのインスタンスの完了を待つ分だけ延びる(最長で制限時間まで。今は落ちた時点で終わる) | -| 運用・保守性 | 起動できなかったインスタンスごとに、名前と理由(終了した・見つからない・時間切れ)が出力に出る。利用者は出力だけで、どのインスタンスの何が欠けたかと、次に打つコマンドを読み取れる | -| セキュリティ | token を書く先は、後処理の対象(案 B の手段では、対象のプロジェクトの動いているインスタンス)に限る。token の値を、出力・ログ・`docker exec` の引数・コンテナの環境変数に載せない(今のまま) | - -## 影響 - -| 対象 | 影響 | -| --- | --- | -| 公開インタフェース | 案 B を採るとき、コマンドかオプションが 1 つ増える(形は未決 2)。既存のコマンド・オプション・環境変数・compose の生成物は変わらない。全インスタンスが起動できたときの `up` / `scale` の終了コードと完了の行は変わらない | -| データ | スキーマの変更も移行も無い。`scale` で増やしたインスタンスのボリュームの `~/.vscode-server/data/Machine/settings.json` に `window.title` が書かれるようになる | -| 既存の振る舞い | (1) `scale` が、増やしたインスタンスへ不足リポジトリの報告と窓のタイトルの設定を行う。(2) `scale` の `./deploy` と token の配布の順が入れ替わる(前提 1)。(3) 起動できなかったインスタンスがあるときの出力が変わる(全部の名前・理由・補い方)。(4) 案 A を採るとき、起動できたインスタンスへは `./deploy` を含む後処理が行われ、1 台が落ちた `up` / `scale` は残りの完了を待ってから終わる。利用者に見える変更のため `CHANGELOG.md` に書く | - -## 検証手段 - -| 項目 | 手段 | -| --- | --- | -| テスト | `env -u DEVBASE_ROOT uv run --locked pytest tests/commands/ tests/utils/ -q`(全体は `.ndf/project.json` の `test.suites` のコマンド) | -| 静的解析・型検査 | `uv run ruff check lib/ tests/`(CI の Ruff の検査ジョブは `check --select=E9,F63,F7,F82 lib` で、その範囲を含む) | -| 起動 | 受け入れ条件 24。`DEVBASE_ROOT` を一時ディレクトリへ向けた使い捨てのプロジェクトで `devbase up` / `devbase scale` を打ち、確かめた後にコンテナとボリュームを消す。利用者の実際のプロジェクトでは打たない | -| 手動確認 | 人が VS Code で、`scale` で増やしたインスタンスへ接続した窓のタイトルがコンテナ名で始まって描かれることを見る(設定ファイルに値が書かれることは受け入れ条件 1・24 で機械が確かめる。#246 の実環境の確認と併せてよい) | - -## 前提とする取り決め - -| 項目 | 参照先 / 決めたこと | -| --- | --- | -| プロジェクト構造 | 起動の後の処理は `lib/devbase/commands/` の `up` / `scale` の側に、起動の待ちは `lib/devbase/utils/` の docker の側に置く(今の置き場のまま)。base イメージと entrypoint(`containers/`)は触らない。テストは `tests/commands/` と `tests/utils/` に置く | -| コーディング規約 | `CONTRIBUTING.md`。Python は CI の Ruff の検査ジョブで確かめる | -| テスト戦略 | コマンドの水準(docker と `subprocess` を差し替える今のハーネス)で、後処理の対象・順序・出力・終了コード(受け入れ条件 1〜18 と採った案の分)を固定する。起動の待ちの結果(どれが起動でき、どれができなかったか)は関数の水準で固定する。実物の docker では、受け入れ条件 24 を完了判定で 1 回確かめる | - -## 境界 - -| 区分 | 内容 | -| --- | --- | -| 常に行う | 既存のテストと Ruff の実行、`CHANGELOG.md` への記入、利用者向けの文書の更新、確かめに使ったコンテナとボリュームの片付け | -| 確認してから行う | 利用者の実際の環境で `devbase up` を打つこと、新しいコマンドかオプションを足すこと(案 B。設計の承認で決める)、既存のテストのうち受け入れ条件 23 に挙げたもの以外を書き換えること | -| 行わない | base イメージと entrypoint の変更(#357 の範囲)、起動できなかったインスタンスの自動の起こし直し、個々の処理の失敗の見せ方の変更、依頼の範囲の外の整形 | - -## 未決 - -| 項目 | 誰が決めるか | 期限 | -| --- | --- | --- | -| 1. 案 A・案 B・両方のどれを採るか。材料: 案 A だけでは、落ちたインスタンスを `docker start` で起こし直した後に、そのインスタンスの処理を補う道が `devbase up` の打ち直し(全インスタンスを作り直す)しか無い。`scale` の後は `devbase scale` が断るため、同じく `up` になる。案 B だけでは、利用者が手段を打つまで、起動できたインスタンスの処理が欠けたままになる | 利用者 | 設計の Pull Request の承認(#371 のコメントの決定) | -| 2. 案 B の手段の形(新しいコマンドにするか、`up` に「動いているインスタンスへ処理だけをやり直す」入口を足すか)と名前 | 設計(利用者が承認する) | 設計の Pull Request の承認 | -| 3. 案 B の手段が `./deploy` を走らせるか。走らせない前提(前提 10)で進める | 利用者 | 設計の Pull Request の承認 | -| 4. `scale` の `./deploy` と token の配布の順を `up` に合わせて入れ替えること(前提 1)でよいか。入れ替える前提で進める。逆に `up` を今の `scale` の順へ合わせると、`up` の `./deploy` が token のある状態で走るようになる | 利用者 | 設計の Pull Request の承認 | -| 5. 案 A で、開く番号のインスタンスが起動できなかったときにエディタを開かないこと(受け入れ条件 A3)でよいか | 設計 | 設計の Pull Request の承認 | -| 6. 受け入れ条件 24 で、使い捨てのプロジェクトの 1 台を起動の待ちの間に終了させる方法 | 設計 | 設計の Pull Request の承認 | diff --git a/issues/issue-334-design-decisions.md b/issues/issue-334-design-decisions.md deleted file mode 100644 index 6d7bce49..00000000 --- a/issues/issue-334-design-decisions.md +++ /dev/null @@ -1,94 +0,0 @@ -# #334: 決定の記録 - -[issue-334-design.md](issue-334-design.md)(#334 の設計)の決定を持つ。不変条件(I1〜I10)と受け入れ条件の番号は、 -設計と要求([issue-334-requirements.md](issue-334-requirements.md))のものを指す。 - -## 決定の記録 - -### 決定 1: 方法 2 の region も取り込みの方針に従わせる - -方法 2 で利用者が入れるのはプロファイル名であり、そのプロファイルの region をホストから参照へ書くことまで -同意したとは読めない(要求の前提 2)。方法 3 だけを直すと、同じ「ホストの `~/.aws/config` から読んで参照へ書く」 -値が、方法によって方針に従ったり従わなかったりする。方法 2 と方法 3 が同じ region の出所の判定を通れば、 -規則が 1 つになり、次に方法を足すときもそれを使う。方法 2 を今のまま残し理由を仕様に書く形は、名前を -明示した利用者が別のグループの region を持ち込む経路を `skip` のグループに残すため採らなかった。 - -根拠: Value 3 / C3(MVV 版 1) - -### 決定 2: `ask` で確認を出さなかったときは、ホストの region を入力の既定の値として見せる - -方針が `ask` で鍵の確認を出さない経路(方法 3 でホストに鍵が無い・方法 2)では、region の入力の既定の値に -ホストの region を出し、どこから来た値かを文に添える(`~/.aws/config の [<節>] から`)。利用者は値と出所を見た -うえで Enter を押すため、確認を経た取り込みになる。増える質問は、ホストに region があるときの入力の 1 回だけである -(今はホストに region があると入力を出さずに書く)。y/N の確認を 1 問足す形は、ホストに region が無いときも -含めて「取り込みますか?」と「region は?」の 2 問を重ね、機密でない値に対して質問がさらに増えるため採らなかった。 - -根拠: Mission / Value 1(MVV 版 1) - -### 決定 3: 方法 3 で鍵の確認を出したときは、その答えを region にも当てる - -方法 3 の鍵の確認は「`~/.aws/credentials` の `[default]` の鍵を取り込むか」を尋ねており、同じ `default` の -プロファイルの region は同じ取り込みの候補に属する。`y` ならホストの region を入力なしで書き(今の `import` と -同じ)、`N` ならホストの region を読まずに `ap-northeast-1` を既定の値とする入力へ進む。断った利用者に、断った -プロファイルの region を既定の値として見せると、断った値を Enter 1 回で取り込む形になる。`y` の後に region の -入力を出す形は、受け入れた候補について 2 度目の確認になるため採らなかった。 - -根拠: Value 3(MVV 版 1) - -### 決定 4: 鍵の確認の結果は aws.py の 3 値で渡し、`HostImport` に記憶させない - -鍵の確認は方法 3 の 1 か所でしか起きない。`_collect_access_keys` が確認の結果を `KeyDecision` にして -`_collect_region` へ引数で渡せば、region の出所の判定は渡された値と方針だけで決まり、テストで値を与えて縛れる。 -`HostImport` に「このステップで既に知らせた・断った」を記憶させて二重の知らせを防ぐ形は、取り込みの場が -collector の手順の状態を持つことになり、GCP と Git の collector の知らせの振る舞いにも効くため採らなかった。 -`HostImport` の変更は、今の `_skipped` を公開の名前(`notify_skipped`)にすることだけにとどめる。 - -根拠: Vision(MVV 版 1) - -### 決定 5: 参照に region が既にあればそれを残すのは方法 3 だけにし、方法 2 は方針に従って決め直す - -要求の退行の条件は「方法 2・方法 3 で `AWS_DEFAULT_REGION` が既に参照にあれば、今と同じくその値を残し、 -ホストを読まない」である。今の方法 3 はそのとおりだが、今の方法 2 は参照の region を見ずに上書きする。 -`env init` で「認証方法を変更しますか?」に `y` と答えると、`AWS_ALL_KEYS` は `AWS_DEFAULT_REGION` を含まないため -前の方法の region が参照に残る。方法 2 でもそれを残すと、新しく入れたプロファイルの region が書かれず、 -`AWS_DEFAULT_REGION` がプロファイルの節の region より優先されるため、別の region へ向く。 - -そこで「今と同じく」を採り、region の出所の判定(決定 1)に `keep_existing` を渡す。方法 3 は `True` で判定の -先頭の「参照に既にある」の枝を通り、方法 2 は `False` でこの枝を通らずに方針の枝で決め直す。方法 2 にも先頭の -枝を効かせる形は、上の持ち越しを生み、方針が `import` の方法 2 の振る舞い(ホストの region で上書き)も変えて -要求の「含まない」と重なるため採らなかった。 - -要求の文(方法 2 を含む)は正である #334 の本文で直し、写しを作り直す。設計の文書の「未確認のまま残ること」に置く。 - -根拠: Value 2 / Vision(MVV 版 1) - -### 決定 6: 正規化と衝突の警告は、取り込む鍵を決めた直後に collector が出す - -警告は「この名前で参照へ書く」ことを利用者に知らせるためにある。取り込むと決まった後にだけ出せば、飛ばした -鍵について出ず(I7)、出すのは collector の 1 か所になる(I6)。アクティブプロファイルの質問より前に出すのは、 -質問が正規化した名前で答えさせるためで、変わった名前を先に見せる。鍵を参照へ書いた後に出す形は、`none` と -答えて鍵を書かないときに警告が出ない利点があるが、質問の時点で利用者が元のファイル名で答えて尋ね直される -ため採らなかった。発見の結果に「既に警告したか」を持たせて呼び出しの 2 回目を黙らせる形は、警告を出す場所が -発見の側に残り、`env sync` だけを打ったときの扱いが別に要るため採らなかった。 - -根拠: Value 1(MVV 版 1) - -### 決定 7: 鍵ファイルの発見を google.py の 1 つにまとめ、sync の引き方を init と同じ規則にそろえる - -今の `env._gcp_profile_files` は、`~/gcp-credentials/` を並べ替えずに読み、正規化した名前が衝突すると後に読んだ -ファイルを採る。`env init` の発見はファイル名の昇順で先のファイルを採る。衝突があると、init が登録した鍵 -ファイルと、控えの更新と sync が引く鍵ファイルが食い違いうる(I9)。発見を `find_credential_files` の 1 つにし、 -`env.py` が結果の `lookup` を使えば、規則が 1 か所に決まり、`_gcp_profile_files` の名前(ファイルの一覧を返す -ように読めるが、名前から鍵ファイルを引く関数を返す)の不揃い(#310 の項)も同時に消える。衝突の無い場合の -振る舞いは変わらない。`env.py` の関数を残して中から google.py を呼ぶ形は、引き方の入口が 2 つ残るため採らなかった。 - -根拠: Vision(MVV 版 1) - -### 決定 8: 発見はプロジェクト ID を読まない - -今の `env init` の発見は、見つけた鍵ファイルをすべて JSON として読み、プロジェクト ID を取り出す。読めない -ファイルがあると `Google credentials JSONのパースに失敗` の警告が出る。発見を `env sync` と控えの更新でも使う -(決定 7)と、今は出ないこの警告が sync でも出るようになる。発見はファイル名と正規化だけを扱い、プロジェクト ID -は GCP の collector が候補を表示するときに読む。`env init` の出力は今と同じになり、sync の出力も今と同じになる。 - -根拠: Value 1(MVV 版 1) diff --git a/issues/issue-334-design.md b/issues/issue-334-design.md deleted file mode 100644 index 199b7f7d..00000000 --- a/issues/issue-334-design.md +++ /dev/null @@ -1,461 +0,0 @@ -# #334: AWS の region と GCP の正規化の警告を取り込みの方針に従わせる - -要求と受け入れ条件は #334 の本文にある(コピーは [issue-334-requirements.md](issue-334-requirements.md))。 -この文書は「どう作るか」だけを扱う。決定の理由は [issue-334-design-decisions.md](issue-334-design-decisions.md) に分けた。 - -変えるのは `env init` の AWS と GCP の collector と、`env sync` と同期済みハッシュの控えの更新が GCP の鍵ファイルを -引く経路である。次の 2 つを入れる。 - -- **region の出所の判定**: AWS の方法 2・方法 3 で、ホストの `~/.aws/config` の region を使うかを、取り込みの方針と - 鍵の確認の答えから 1 か所で決める。方針が `skip` のときと `ask` で断ったときは、ホストの region を読まない -- **警告を出さない鍵ファイルの発見**: `~/gcp-credentials/` の発見は出力を持たない 1 つの関数にし、`env init` と - `env sync` / 控えの更新が同じものを使う。正規化と衝突の警告は、取り込むと決めた鍵についてだけ collector が出す - -## ドメインモデル - -### コンテキスト - -| コンテキスト | 何の語が 1 つの意味に決まるか | -| --- | --- | -| 機密の置き場(`secret`) | 取り込み・取り込みの方針・取り込みの候補・取り込みの選択・AWS のプロファイル・プロファイル名の正規化と、この設計で足すホストの region・region の出所・鍵の確認の結果・鍵ファイルの発見 | - -変更は 1 つのコンテキストで閉じる。取り込みの方針の決め方(`host-import.yml` と `host_import.resolve`)は #314 の -ものをそのまま使い、変えない(要求の前提 8)。 - -### 集約 - -| 集約 | 持ち主(書き換えてよいもの) | 根 | エンティティ | 値オブジェクト | -| --- | --- | --- | --- | --- | -| 取り込みの場 | `host_import.HostImport`(方針と、飛ばしたことの知らせ) | 1 回の `env init` の取り込みの場 | — | 取り込みの方針・端末でないため落ちたか・取り込みの選択 | -| AWS の認証の収集 | `collectors/aws.py` | 1 回の AWS の認証方法の選択 | — | 鍵の確認の結果(受け入れた / 断った / 確認しなかった)・region の出所(ホスト / 入力 / 既定の値 / 参照に既にある) | -| GCP の鍵ファイルの発見 | `collectors/google.py` | 1 回の `~/gcp-credentials/` の発見の結果 | — | プロファイル名 → 鍵ファイル・正規化で変わった名前 → 元の名前・衝突で外した鍵ファイル | - -GCP の鍵ファイルの発見の持ち主は `google.py` だけである。`commands/env.py` は発見の結果を読むだけで、自分で -ディレクトリを読まない。 - -### 不変条件 - -| # | 集約 | 条件 | 破れたときの扱い | -| --- | --- | --- | --- | -| I1 | AWS の認証の収集 | 方法 2・方法 3 で、方針が `skip`(端末でない `ask` から落ちたものを含む)のとき、または方法 3 の鍵の確認で断ったとき、ホストの `~/.aws/config` の region を読まず、参照へ書かない。region は入力(既定の値 `ap-northeast-1`)で決める | 確認なしにホストの値が参照へ入る(#314 の「確認なしに取り込まない」から外れる)。テストが落とす | -| I2 | AWS の認証の収集 | 方針が `import` のとき(方法 2・方法 3)と、方法 3 の鍵の確認で受け入れたときは、ホストの region があれば入力を出さずに書き、無ければ入力へ進む | 今の `import` の振る舞いが変わる、または受け入れた答えが region に効かない。テストが落とす | -| I3 | AWS の認証の収集 | 方針が `ask` で鍵の確認を出さなかったとき(方法 3 でホストに鍵が無い・方法 2)、ホストの region を入力の既定の値として見せる。空の答えはその値、入れた値はその値を書く。ホストに region が無ければ既定の値は `ap-northeast-1` | 利用者が見ていない値が書かれる、または入れた値が無視される。テストが落とす | -| I4 | AWS の認証の収集 | 方法 3 で参照に `AWS_DEFAULT_REGION` が既にあれば、その値を残し、ホストの region を読まず、入力も出さない。方法 2 は参照の region を見ずに、I1〜I3 のとおり方針に従って決め直す(今の方法 2 も参照の region を上書きする) | 方法 3 で既にある region が上書きされる、または方法 2 で前の認証方法の region が新しいプロファイルへ持ち越される。テストが落とす | -| I5 | 取り込みの場 | 方法 2・方法 3 の 1 回の選択で、方針が `skip` のために飛ばした知らせは 1 行までにする。鍵の確認で既に知らせたときは region のために足さない。`ask` で断ったときは今の「取り込みません」の 1 行だけにする | 同じ選択で同じ知らせが 2 行出る。テストが落とす | -| I6 | GCP の鍵ファイルの発見 | 鍵ファイルの発見は、ログも標準出力も出さない。正規化と衝突の結果は値として返す | 発見を呼ぶたびに警告が出る(今の不具合)。テストが落とす | -| I7 | GCP の鍵ファイルの発見 | 正規化の警告は、取り込むと決めた名前のうち正規化で変わったものについて、名前ごとに 1 回出す。1 回の `env init` で同じ名前について 2 回出さない。取り込まないと決めた名前と、`env sync` と控えの更新では出さない | 飛ばしたのに警告が出る、または 2 回出る。テストが落とす | -| I8 | GCP の鍵ファイルの発見 | 衝突の警告(後の鍵ファイルを候補から外した)は、取り込むと決めた名前のうち衝突で外した鍵ファイルがあるものについて、名前ごとに 1 回出す。I7 と同じ時機と範囲で出す | `skip` で衝突の警告が出る、または `import` で出ない。テストが落とす | -| I9 | GCP の鍵ファイルの発見 | `env init` の登録と、`env sync` と控えの更新がプロファイル名から引く鍵ファイルは、同じ発見の結果から決まる(ファイル名の昇順で並べ、正規化した名前が衝突したら先のファイルを採る。名前が `default` で発見に無ければ `~/google_credential.json` を引く) | init で登録した鍵ファイルと、sync が入れ直す鍵ファイルが食い違う。テストが落とす | -| I10 | GCP の鍵ファイルの発見 | 参照へ書くキーと値(`GCP_CREDENTIALS_BASE64_<名前>`・`GCP_ACTIVE_PROFILE`・`GOOGLE_CLOUD_PROJECT` ほか)は、どの方針でも変更の前と同じになる | 取り込んだ結果が変わる。テストが落とす | - -### ドメインイベント - -要求の E1〜E9 を引き継ぐ。足すイベントは無い。AWS は E1 → E2 →(方法 3 は E3)→ E4 → E5、GCP は E1 → E6 → E7 → E8 → -(init の終わりに)E9 の順に起きる。 - -| # | イベント | 発生元 | 受け手 | -| --- | --- | --- | --- | -| E1 | 取り込みの方針が決まった | `cmd_env_init`(`host_import.resolve`) | AWS と GCP の collector | -| E2 | AWS の認証方法が選ばれた | `collect_aws_credentials` | 方法ごとの収集の関数 | -| E3 | 方法 3 でホストの鍵の扱いが決まった | `_collect_access_keys` | region の出所の判定(鍵の確認の結果として渡す) | -| E4 | region の出所が決まった | region の出所の判定 | 同じ関数(書き込みへ進む) | -| E5 | `AWS_DEFAULT_REGION` を参照へ書いた | region の出所の判定 | `env init` の保存 | -| E6 | GCP の鍵ファイルの候補を見つけた | 鍵ファイルの発見 | GCP の collector と、sync / 控えの更新の鍵ファイルの引き方 | -| E7 | 取り込む GCP の鍵が決まった | GCP の collector(`host.choose` の結果) | 正規化と衝突の警告 | -| E8 | 正規化と衝突の警告を出した | GCP の collector | 利用者(端末の出力) | -| E9 | 同期済みハッシュの控えを更新した | `_record_gcp_source` / `_sync_gcp` | 同期済みハッシュの控え | - -### 用語 - -| 用語 | 意味 | 用語集への反映 | -| --- | --- | --- | -| ホストの region | ホストの `~/.aws/config` の、選んだ AWS のプロファイルの節にある `region` の値。方法 3 は `[default]`、方法 2 は利用者が入れたプロファイル | 追加(`secret`) | -| region の出所 | 参照の `AWS_DEFAULT_REGION` へ書く値をどこから得たか。参照に既にある・ホストの region・利用者の入力・既定の値(`ap-northeast-1`)の 4 つ | 追加(`secret`) | -| 鍵の確認の結果 | 方法 3 で、ホストの `[default]` の鍵を取り込むかを決めた結果。受け入れた・断った・確認しなかった(ホストに鍵が無い)の 3 つ。方針が `import` なら受け入れた。`skip` ならホストに鍵があれば断った、無ければ確認しなかったになる | 追加(`secret`) | -| 鍵ファイルの発見 | `~/gcp-credentials/` の `*.json` を読み、正規化したプロファイル名から鍵ファイルへの対応を作ること。出力を持たない | 追加(`secret`) | -| プロファイル名の正規化 | 要求の段階で足した語。意味は変えず、出所をこの設計へ移す | 追加済み(`secret`、要求の段階)。出所を変更 | - -## 機能一覧 - -| # | 機能 | 誰が使うか | -| --- | --- | --- | -| F1 | 方法 3(Access Key)で、取り込まない設定のグループにホストの region を書かずに region を決める | `host-import.yml` でグループを `skip` にした利用者と、端末でない `env init` | -| F2 | 方法 3 で、鍵の確認の答えを region にも当てる | 方針が `ask` のグループで `env init` を打つ利用者 | -| F3 | 方法 2・方法 3 で、確認を出さなかったときにホストの region を入力の既定の値として見せる | 方針が `ask` のグループの利用者 | -| F4 | 方法 2(SSO Profile)の region を取り込みの方針に従わせる | 方法 2 を選ぶ利用者 | -| F5 | 取り込むと決めた GCP の鍵についてだけ、正規化と衝突の警告を 1 回出す | `~/gcp-credentials/` に `-` などを含む名前の鍵を置く利用者 | -| F6 | `env sync` と控えの更新で、GCP の鍵ファイルを警告なしに、`env init` と同じ規則で引く | `env sync` を打つ利用者 | - -## 構成要素 - -| 要素 | 責務 | 新設 / 変更 | -| --- | --- | --- | -| 飛ばしたことの知らせ(`HostImport.notify_skipped`) | 方針が `skip` のために飛ばしたことを、今の 2 つの文(`取り込まない設定のため飛ばしました` / `標準入力が端末でないため飛ばしました`)で 1 行出す。今の `_skipped` を公開の名前にしたもので、`choose` と `confirm` もこれを呼ぶ | 変更(名前を公開にする) | -| 鍵の確認の結果(`aws.KeyDecision`) | `ACCEPTED` / `DECLINED` / `NOT_ASKED` の 3 値の `Enum` | 新設 | -| region の出所の判定(`aws._collect_region`) | 参照の region・方針・鍵の確認の結果から region の出所を決め、`AWS_DEFAULT_REGION` を書く。参照の region を残すかは呼び元が `keep_existing` で渡す(方法 3 は `True`、方法 2 は `False`)。ホストの region は出所がホストになりうるときだけ、`AWSConfigParser.find_profile_region` で節の名前と一緒に読む。方法 2・方法 3 が共有する | 新設 | -| 節の名前と region の読み取り(`AWSConfigParser.find_profile_region`) | プロファイル名から、今の `get_profile_region` と同じ規則(`default` は `[default]`、ほかは `[profile <名前>]`、無ければ `[<名前>]`)で節を引き、`(節の名前, region)` を返す。節か region が無ければ `None`。`get_profile_region` はこれを呼んで region だけを返す形にし、戻り値と振る舞いを変えない | 新設 | -| Access Key の収集(`aws._collect_access_keys`) | 鍵の扱いを今のまま決め、その結果を鍵の確認の結果にして region の出所の判定へ渡す(`keep_existing=True`)。今の末尾の region の処理を外す | 変更 | -| SSO Profile の収集(`aws._collect_sso_profile`) | `host` を受け取り、region を region の出所の判定(鍵の確認の結果は `NOT_ASKED`、`keep_existing=False`)に任せる。プロファイル名の一覧の表示と `AWS_SSO_URL` の入力は変えない | 変更(引数に `host` を足す) | -| AWS の認証の入口(`aws.collect_aws_credentials`) | 方法 2 の処理へ `host` を渡す | 変更(1 行) | -| 正規化(`google._safe_profile_name`) | 名前を正規化して返すだけにし、警告を出さない | 変更 | -| 鍵ファイルの発見(`google.find_credential_files`) | `~/gcp-credentials/` をファイル名の昇順に読み、`CredentialFiles`(名前 → 鍵ファイル・正規化で変わった名前 → 元の名前・名前 → 衝突で外した鍵ファイルの並び)を返す。ディレクトリに鍵が無ければ `~/google_credential.json` を `default` として返す。JSON を読まず、何も出力しない | 新設(今の `_discover_credential_files` を置き換える) | -| 発見の結果(`google.CredentialFiles`) | 上の 3 つを持つ `dataclass`。`lookup(name)` で鍵ファイルを引く(名前が `default` で無ければ `~/google_credential.json`) | 新設 | -| 正規化と衝突の警告(`google._warn_for_chosen`) | 取り込むと決めた名前の並びと発見の結果を受け、正規化で変わった名前と、衝突で外した鍵ファイルがある名前について、今と同じ文の警告を 1 回ずつ出す | 新設 | -| GCP の collector(`google.collect_google_credentials`) | 発見の結果から候補を並べ、プロジェクト ID は候補の表示のときに読む。取り込む鍵が決まった直後(アクティブプロファイルを尋ねる前)に正規化と衝突の警告を出す | 変更 | -| GCP の鍵ファイルの引き方(`commands/env.py` の `_sync_gcp`・`_record_gcp_source`) | `google.find_credential_files()` の結果の `lookup` で鍵ファイルを引く。`_gcp_profile_files` を消す | 変更 | -| collector の単体テスト(`tests/env/test_collector_host.py`) | AWS の方法 2・方法 3 の受け入れ条件と I1〜I5 を縛る | 変更(テストを足す) | -| GCP の単体テスト(`tests/env/test_gcp_auth.py`) | I6〜I9 のうち、発見と警告の関数の単位で縛れるものを縛る | 変更(テストを足す) | -| コマンドの層のテスト(`tests/commands/test_env_host_import.py`) | 1 回の `env init` と `env sync` での警告の回数(受け入れ条件の GCP の 7 行)と I10 を縛る | 変更(テストを足す) | -| 変更履歴(`CHANGELOG.md`) | `[Unreleased]` の `Fixed` に、方法 2・方法 3 の region が方針に従うことと、GCP の警告の回数を書く | 変更 | -| 用語集(`docs/glossary/glossary.json` と `docs/glossary.md`) | 用語の表の 4 語を `secret` に足し、「プロファイル名の正規化」の出所をこの設計へ移す | 変更(設計の Pull Request で行う) | - -変えない関数は、`AWSConfigParser` の既存のメソッド(`get_profile_region` は中で `find_profile_region` を呼ぶ形にするが、戻り値と振る舞いは変えない)・方法 1 の `_collect_profile_and_region`・ -`_collect_config_base64`・`_collect_selected_profiles`・`host_import.resolve` / `load` である。方法 1 の region は -要求の前提 5 のとおり変えない。 - -変える関数の呼び出し元は次のとおりである。 - -| 関数 | 呼び出し元 | 直すか | -| --- | --- | --- | -| `HostImport._skipped`(→ `notify_skipped`) | `HostImport.choose`・`HostImport.confirm` | 直す(呼ぶ名前だけ) | -| `aws._collect_sso_profile` | `collect_aws_credentials` の方法の表 | 直す(`host` を渡す) | -| `aws._collect_access_keys` | `collect_aws_credentials` の方法の表 | 直さない(引数の形は変えない) | -| `AWSConfigParser.get_profile_region`(中身だけ `find_profile_region` へ寄せる) | `_collect_profile_and_region`(方法 1)・`_collect_sso_profile`・`_collect_access_keys` | 方法 1 は直さない(戻り値が同じ)。方法 2・方法 3 の呼び出しは `_collect_region` の `find_profile_region` に置き換わる | -| `google._safe_profile_name` | `_discover_credential_files`(消す)・`env._gcp_profile_files`(消す) | 呼び出し元ごと置き換わる | -| `google._discover_credential_files` | `collect_google_credentials` | `find_credential_files` へ置き換える | -| `env._gcp_profile_files` | `_sync_gcp`・`_record_gcp_source` | `google.find_credential_files().lookup` へ置き換える | - -既存のテストは、`google.GCP_CREDENTIALS_DIR` と `google.LEGACY_CREDENTIALS_FILE` をモジュールの属性として差し替える -(`tests/commands/test_env_host_import.py`・`tests/commands/test_env_sync_owner.py`)。発見の関数はこの 2 つを呼び出しの -時点で読み、差し替えが効く形を保つ。消す関数と名前を変える関数を、既存のテストは直に呼んでいない -(`tests/` を関数名で検索して 0 件)。 - -### 文脈 - -```mermaid -graph TD - U[利用者] -->|env init・env sync| D[devbase] - H[host-import.yml] -->|取り込みの方針| D - A[ホストの ~/.aws/config
と credentials] -->|方針に従って読む| D - G[ホストの
~/gcp-credentials/] -->|発見| D - D -->|書く| R[参照
(グループの共通の機密)] - D -->|書く| S[同期済みハッシュの控え] -``` - -変えられない外部は、ホストの資格情報ファイルと `host-import.yml` の形である(要求の前提 8)。 - -### 構成要素の関係 - -```mermaid -graph TD - subgraph host_import - HI[HostImport
notify_skipped・choose・confirm] - end - subgraph aws - AE[collect_aws_credentials] - AK[_collect_access_keys] - AS[_collect_sso_profile] - AR[_collect_region] - KD[KeyDecision] - AP[AWSConfigParser] - end - subgraph google - GC[collect_google_credentials] - GF[find_credential_files] - CF[CredentialFiles.lookup] - GW[_warn_for_chosen] - SN[_safe_profile_name] - end - subgraph envcmd[env コマンド] - INIT[cmd_env_init] - SY[_sync_gcp] - RG[_record_gcp_source] - end - INIT --> AE - INIT --> GC - INIT --> RG - AE --> AK - AE --> AS - AK --> HI - AK --> KD - AK --> AR - AS --> AR - AR --> HI - AR --> AP - GC --> GF - GC --> HI - GC --> GW - GF --> SN - SY --> CF - RG --> CF - CF --> GF -``` - -テスト・変更履歴・用語集の 4 つは、コードの呼び出しの関係に入らないため図に含めない。 - -### モジュールの構成 - -```text -lib/devbase/ -├── env/ -│ ├── host_import.py # notify_skipped を公開にする -│ └── collectors/ -│ ├── aws.py # KeyDecision・_collect_region を足し、方法 2・方法 3 が使う -│ └── google.py # find_credential_files・CredentialFiles・_warn_for_chosen -└── commands/ - └── env.py # _gcp_profile_files を消し、google の発見を使う -tests/ -├── env/ -│ ├── test_collector_host.py # AWS の方法 2・方法 3 -│ └── test_gcp_auth.py # 発見と警告 -└── commands/ - └── test_env_host_import.py # 1 回の init・sync での警告の回数 -``` - -## 構造 - -```mermaid -classDiagram - class HostImport { - +policy: ImportPolicy - +fell_back: bool - +asking: bool - +importing: bool - +notify_skipped(step) - +declined(step) - +choose(step, title, labels, hint) Selection - +confirm(step, question) bool - } - class KeyDecision { - <> - ACCEPTED - DECLINED - NOT_ASKED - } - class CredentialFiles { - +files: dict - +renamed: dict - +collisions: dict - +lookup(name) Path or None - } - class AWSConfigParser { - +get_profile_region(profile) str or None - +find_profile_region(profile) tuple or None - +get_default_credentials() tuple - } - class aws_collector { - <> - +_collect_access_keys(env_file, host) - +_collect_sso_profile(env_file, host) - +_collect_region(env_file, parser, profile, host, decision, keep_existing) - } - class google_collector { - <> - +find_credential_files() CredentialFiles - +_warn_for_chosen(chosen, found) - } - aws_collector ..> HostImport : 方針を読む・知らせる - aws_collector ..> KeyDecision : 作って渡す - aws_collector ..> AWSConfigParser : 読む枝だけで呼ぶ - google_collector ..> CredentialFiles : 作る -``` - -`_collect_region(env_file, parser, profile, host, decision, keep_existing)` は戻り値を持たず、参照へ書く。`KeyDecision` を -`HostImport` に持たせないのは、鍵の確認が方法 3 の 1 か所でだけ起きるためである(決定 4)。 - -`CredentialFiles.files` は挿入の順(ファイル名の昇順)を保つ。`renamed` と `collisions` のキーは `files` のキー -(正規化した名前)である。`collisions` の値は、同じ名前に正規化され、先のファイルに負けて外した鍵ファイルの並び。 - -## 入出力の契約 - -コマンドとオプションは変わらない。変わるのは、方針が `skip` / `ask` の方法 2・方法 3 で、region の入力が出るかと、 -その既定の値と、知らせの行である。region の出所ごとの出力は次のとおり。 - -| region の出所 | 出す行(標準出力の入力 / ログ) | -| --- | --- | -| 参照に既にある(方法 3 だけ) | ログ `AWS_DEFAULT_REGION: 設定済み (<値>)`(今と同じ) | -| ホストの region(入力なし) | ログ `AWS_DEFAULT_REGION: 自動取得完了 (<値>)`(今と同じ) | -| 入力(既定の値がホストの region) | 入力 `AWS_DEFAULT_REGION (デフォルト: <値>、~/.aws/config の [<節>] から): ` | -| 入力(既定の値が `ap-northeast-1`) | 入力 `AWS_DEFAULT_REGION (デフォルト: ap-northeast-1): `(今と同じ) | - -`<節>` は `AWSConfigParser.find_profile_region` が返す節の名前である。方法 3 で `default`、方法 2 で `profile <名前>` -(`[profile <名前>]` が無く `[<名前>]` で引けたときは `<名前>`)になる。 - -飛ばしたことの知らせは、方針が `skip` で鍵の確認の結果が `NOT_ASKED` のときだけ region の出所の判定が出す -(`HostImport.notify_skipped("AWS認証")`。文は今の `_skipped` のまま)。 - -| 方法 | 方針 | ホストの鍵 | 知らせの行 | -| --- | --- | --- | --- | -| 方法 3 | `skip` | ある | `AWS認証: 取り込まない設定のため飛ばしました (<グループ>)` が 1 行(`confirm` が出す。region では足さない) | -| 方法 3 | `skip` | 無い | 同じ文が 1 行(region の出所の判定が出す) | -| 方法 3 | `ask` | ある・断った | `AWS認証: 取り込みません` が 1 行(`declined` が出す。region では足さない) | -| 方法 2 | `skip` | — | 同じ文が 1 行(region の出所の判定が出す。今は出ない) | - -端末でない `ask` から落ちた `skip` では、文が `標準入力が端末でないため飛ばしました` になる(今の `_skipped` のまま)。 - -GCP の警告の文は変えない(`プロファイル名 '<元>' を '<名前>' に正規化しました` と -`プロファイル名 '<名前>' が衝突しています: '<採ったファイル>' と '<外したファイル>' (後者をスキップ)`)。変わるのは -出す時機と回数である(I7・I8)。衝突の警告は外した鍵ファイル 1 つにつき 1 行で、名前ごとに 1 回の発見で 1 度だけ出す。 - -## 処理の流れ - -### region の出所の判定 - -```mermaid -graph TD - S[_collect_region] --> E{keep_existing かつ
参照に region がある} - E -->|ある| K[残す
設定済み] - E -->|無い| D{方針・鍵の確認の結果} - D -->|import
または ACCEPTED| H[ホストの region を読む] - H -->|ある| W[書く
自動取得完了] - H -->|無い| I1[入力
既定 ap-northeast-1] - D -->|DECLINED| I1 - D -->|skip かつ NOT_ASKED| N[notify_skipped] --> I1 - D -->|ask かつ NOT_ASKED| H2[ホストの region を読む] - H2 -->|ある| I2[入力
既定 ホストの region] - H2 -->|無い| I1 -``` - -判定の順序は「参照に既にある」が先である。方法 3(`keep_existing=True`)は、方針が `skip` でも、参照に region が -あれば知らせを出さずに残す(ホストを読まないため、飛ばしたものが無い)。方法 2(`keep_existing=False`)はこの枝へ -入らず、参照に region があっても方針の枝で決め直す。認証方法を変えたとき `AWS_ALL_KEYS` は `AWS_DEFAULT_REGION` を -消さないため、前の方法の region を新しいプロファイルへ持ち越さないためである(今の方法 2 も上書きする)。 - -方針が `skip` の方法 3 の鍵の確認は、ホストに鍵があれば `DECLINED` になり(`confirm` が `False` を返し、知らせを出す)、鍵が無ければ `NOT_ASKED` になる。 -方法 3 で鍵の確認の結果を決める表は次のとおり。 - -| 方針 | ホストの `[default]` の鍵 | 鍵の確認の結果 | -| --- | --- | --- | -| `import` | ある / 無い | `ACCEPTED` | -| `ask` | ある・`y` | `ACCEPTED` | -| `ask` | ある・`N`(EOF を含む) | `DECLINED` | -| `ask` | 無い | `NOT_ASKED` | -| `skip` | ある | `DECLINED` | -| `skip` | 無い | `NOT_ASKED` | - -方法 2 は常に `NOT_ASKED` を渡す。`import` は鍵の確認の結果によらずホストの region を読むため、`import` の行の -`ACCEPTED` は判定の図の同じ枝へ入る。 - -### 方法 3 の流れ - -```mermaid -sequenceDiagram - participant C as collect_aws_credentials - participant K as _collect_access_keys - participant H as HostImport - participant P as AWSConfigParser - participant R as _collect_region - C->>K: 方法 3 - K->>P: get_default_credentials() - alt 鍵があり import でない - K->>H: confirm("取り込みますか?") - H-->>K: True / False(skip は知らせて False) - end - K->>K: 鍵の 2 問(今のまま) - K->>R: decision - R->>R: 参照の region を見る - opt ホストの region を読む枝 - R->>P: find_profile_region("default") - end - R->>H: notify_skipped(skip かつ NOT_ASKED のときだけ) - R->>R: 書く / 入力 -``` - -方法 2 は、`_collect_sso_profile` がプロファイル名の一覧を表示し、プロファイル名を受け取って `AWS_PROFILE` を -書いた後に、`_collect_region(..., profile=<名前>, decision=NOT_ASKED, keep_existing=False)` を呼ぶ。`AWS_SSO_URL` の入力はその後で -今のまま行う。プロファイル名が空なら今と同じく何も書かずに戻り、region を決めない。 - -### GCP の `env init` の流れ - -```mermaid -sequenceDiagram - participant I as cmd_env_init - participant G as collect_google_credentials - participant F as find_credential_files - participant H as HostImport - participant W as _warn_for_chosen - participant R as _record_gcp_source - I->>G: collect_fn(env_file, host) - G->>F: 発見(出力なし) - F-->>G: CredentialFiles - G->>H: choose(import は全部・skip は知らせて 0 件) - H-->>G: 取り込む名前 - alt 1 件以上 - G->>W: 取り込む名前・CredentialFiles - W-->>G: 正規化と衝突の警告を 1 回ずつ - G->>G: アクティブプロファイル・鍵の登録(今のまま) - end - I->>R: 控えの更新 - R->>F: 発見(出力なし) - F-->>R: CredentialFiles.lookup -``` - -`env sync` の `_sync_gcp` も、控えの更新と同じく `find_credential_files().lookup` で鍵ファイルを引き、警告を出さない。 -候補が 0 件で手入力へ進む経路(`~/gcp-credentials/` も `~/google_credential.json` も無い)は変えない。 - -アクティブプロファイルに `none` と答えて鍵を書かないときも、警告は出した後である。警告は取り込む鍵を選んだ -時点の名前について出し、利用者がアクティブプロファイルの質問で正規化した名前を答えられるようにする(決定 6)。 - -## 非機能の実現方式 - -| 大項目 | 条件 | 実現方式 | -| --- | --- | --- | -| セキュリティ | 方針が `skip`(端末でない `ask` を含む)のとき、方法 2・方法 3 は `~/.aws/config` の region を参照へ書かない | region の出所の判定が、`skip` と `DECLINED` の枝で `AWSConfigParser.find_profile_region` と `get_profile_region` を呼ばない(I1)。テストは偽の HOME に置いた `~/.aws/config` を使い、両方を呼んだら失敗する差し替えでも縛る | -| 運用・保守性 | 飛ばしたときの知らせは今と同じ形の 1 行で、1 回の選択で 1 行まで。新しい警告の種類を足さない | 知らせは `HostImport.notify_skipped` の今の 2 つの文だけを使う。region の出所の判定は `NOT_ASKED` のときだけ知らせ、鍵の確認で知らせた選択では足さない(I5)。ホストの region を既定の値として見せる入力の文は入力であり、警告ではない | - -## 決定の記録 - -[issue-334-design-decisions.md](issue-334-design-decisions.md) に分けた。結論だけを並べる。 - -| # | 結論 | -| --- | --- | -| 1 | 方法 2 の region も取り込みの方針に従わせる(要求の未決 1) | -| 2 | `ask` で確認を出さなかったときは、y/N の質問を足さず、ホストの region を入力の既定の値として見せる(要求の未決 2) | -| 3 | 方法 3 で鍵の確認を出したときは、その答えを region にも当てる | -| 4 | 鍵の確認の結果は aws.py の 3 値で渡し、`HostImport` に記憶させない | -| 5 | 参照に region が既にあればそれを残すのは方法 3 だけにし、方法 2 は方針に従って決め直す | -| 6 | 正規化と衝突の警告は、取り込む鍵を決めた直後に collector が出す | -| 7 | 鍵ファイルの発見を google.py の 1 つにまとめ、sync の引き方を init と同じ規則(昇順・先勝ち)にそろえる | -| 8 | 発見はプロジェクト ID を読まない | - -## テスト設計 - -AWS の行は、偽の HOME の `~/.aws/config` に `[default]` と `[profile dev]` の `region = ap-southeast-2` を置いて縛る。 - -| 受け入れ条件・不変条件 | どの振る舞いで縛るか | どう壊したら落ちるべきか | -| --- | --- | --- | -| 方法 3・`skip`・鍵と region を空で答える(AC 1)、I1 | 参照の region が `ap-northeast-1` になり、`ap-southeast-2` が書かれない | `skip` の枝でホストの region を読むように壊す | -| 方法 3・`skip` で「自動取得完了」が出ない(AC 2) | 出力に `AWS_DEFAULT_REGION: 自動取得完了` が無い | 同上 | -| 端末でない `ask` から落ちた `skip`(AC 3)、I1 | `resolve(..., interactive=False)` の場で方法 3 を選ぶと `ap-southeast-2` が書かれない | `fell_back` の `skip` を `ask` として扱うように壊す | -| 方法 3・`ask`・鍵あり・`N`(AC 4)、I1 | region の入力の既定の値が `ap-northeast-1` で、空の答えで `ap-northeast-1` | `DECLINED` をホストの region を既定の値として見せる枝へ入れるように壊す | -| 方法 3・`ask`・鍵あり・`y`(AC 5)、I2 | region の入力が出ずに `ap-southeast-2` | `ACCEPTED` を入力の枝へ入れるように壊す | -| 方法 3・`ask`・鍵なし(AC 6)、I3 | 入力の文に `ap-southeast-2` が既定の値として出て、空なら `ap-southeast-2`、`us-east-1` と入れれば `us-east-1` | 入れた値を捨ててホストの region を書くように壊す / 既定の値を `ap-northeast-1` に戻す | -| 方法 3・`import`(AC 7)、I2 | 入力なしで `ap-southeast-2` | `import` を `ask` と同じ枝へ入れるように壊す | -| 方法 2・`skip`(AC 8)、I1 | 空の答えで `ap-northeast-1`。`find_profile_region` と `get_profile_region` を呼んだら失敗する差し替えでも通る | 方法 2 で方針を見ずにホストの region を読むように壊す | -| 方法 2・`ask`(AC 9)、I3 | 入力の既定の値に `ap-southeast-2` が出て、空なら `ap-southeast-2` | 方法 2 を `skip` と同じ枝へ入れるように壊す | -| 方法 2・`import`(AC 10)、I2 | 入力なしで `ap-southeast-2` | 方法 2 の `import` で入力を出すように壊す | -| 退行: #314 の鍵の扱い(AC 退行 1) | 方法 3 の `skip` / `ask` で断る / `import` で、鍵が書かれない / 書かれない / 書かれる(今のテストのまま) | 鍵の確認の結果を region のために変えて鍵の扱いへ戻すように壊す | -| 退行: 参照に region がある(AC 退行 2)、I4 | 方法 3 のどの方針でも元の値が残り、入力が出ず、`find_profile_region` を呼ばない | 判定の順序を入れ替えて方針を先に見るように壊す | -| I4(方法 2 で参照に region がある) | 参照に `AWS_DEFAULT_REGION=us-west-2` を置いて方法 2 で `dev` を入れると、`import` は入力なしで `ap-southeast-2`、`skip` は空の答えで `ap-northeast-1`、`ask` は既定の値に `ap-southeast-2` が出る。`設定済み` のログが出ない | 方法 2 にも `keep_existing=True` を渡すように壊す | -| 退行: 方法 1 の region(AC 退行 3) | 今の方法 1 のテストがそのまま通る | `_collect_profile_and_region` を `_collect_region` へ置き換えるように壊す | -| I5 | 方法 3・`skip`・鍵あり、方法 3・`skip`・鍵なし、方法 2・`skip` のそれぞれで、`飛ばしました` の行がちょうど 1 行。`ask` で断ると `取り込みません` が 1 行で、`飛ばしました` は出ない | region の出所の判定が `DECLINED` でも知らせるように壊す | -| GCP・`skip`(AC 11)、I7 | 1 回の `env init` で正規化の警告が 0 回 | 発見の中で警告を出すように戻す | -| GCP・`ask` で断る(AC 12)、I7 | 正規化の警告が 0 回 | 警告を選択の前に出すように壊す | -| GCP・`ask` で `plain` だけ(AC 13)、I7 | 正規化の警告が 0 回 | 警告を候補の全部について出すように壊す | -| GCP・`ask` で `my_proj`(AC 14)、I7 | 正規化の警告がちょうど 1 回(控えの更新を含む `env init` の全体で数える) | 控えの更新の引き方が警告を出すように戻す | -| GCP・`import`(AC 15)、I7 | 正規化の警告がちょうど 1 回 | 同上 | -| GCP の衝突(AC 16)、I8 | `a-b.json` と `a_b.json` で、`skip` は衝突の警告が 0 回、`import` はちょうど 1 回 | 衝突の警告を発見の中で出す / 取り込む名前について出さないように壊す | -| GCP の `env sync`(AC 17)、I6・I9 | 正規化の警告が 0 回で、`GCP_CREDENTIALS_BASE64_my_proj` の入れ直しに `my-proj.json` を使う | sync の引き方が警告を出す / 正規化しない名前で引くように壊す | -| I6 | 発見の関数を呼ぶ前後で、ログと標準出力に何も出ない | 発見の中に警告を戻す | -| I9 | `a-b.json` と `a_b.json` で、init が登録する鍵ファイルと、控えの更新と sync が引く鍵ファイルが同じ(`a-b.json`) | sync の引き方を後勝ちに戻す | -| I9(`~/google_credential.json`) | ディレクトリに `default` が無く `~/google_credential.json` があれば、`lookup("default")` が後者を返す | `lookup` の `default` の受け皿を外す | -| 退行: GCP のキーと値(AC 退行 4)、I10 | 3 つの方針で、参照のキーと値の集合が変更の前のテストの期待と同じ | 発見の順序や採るファイルを変える | -| 退行: 既存のテスト(AC 退行 5) | `tests/env`・`tests/commands` の既存のテストがすべて通る | — | - -## 未確認のまま残ること - -| 項目 | 内容 | -| --- | --- | -| 方法 2 で参照に region が既にあるときの要求の文 | 要求の退行の条件「方法 2・方法 3 で `AWS_DEFAULT_REGION` が既に参照にあれば、今と同じくその値を残し、ホストを読まない」は、方法 2 について今の振る舞いと違う(今の方法 2 は参照の region を上書きする)。設計は「今と同じく」を採り、残すのを方法 3 だけにした(I4・決定 5)。方法 2 の `import` の振る舞いも今のまま(ホストの region で上書き)になる。要求の文を方法 3 に限る直しは正(#334 の本文)で行い、`spec-copy.py write` で写しを作り直す。設計の Pull Request で利用者が確かめる | -| 方法 2 の `skip` で増える知らせの 1 行 | 今の方法 2 は方針を見ないため知らせを出さない。この変更で `AWS認証: 取り込まない設定のため飛ばしました` が 1 行増える。非機能の条件の「同じ形の 1 行」に当たると読んだ | -| 手動確認 | 要求の検証手段の手動確認(一時的な `DEVBASE_ROOT` と偽の HOME で `env init`)は実装の後に行う。実際の利用者の環境では行わない | diff --git a/issues/issue-334-requirements.md b/issues/issue-334-requirements.md deleted file mode 100644 index 05be2156..00000000 --- a/issues/issue-334-requirements.md +++ /dev/null @@ -1,183 +0,0 @@ -# #334: env init の AWS の方法 3 が取り込みの方針によらず AWS_DEFAULT_REGION をホストから書く - -正は課題の本文(#334)で、この文書はその写しである。`spec-copy.py write` で作り直す。手で直さない。 - -## 依頼(原文) - -> ## 何が起きるか -> -> 1. **AWS の方法 3(Access Key)で、取り込みの方針に関係なく `AWS_DEFAULT_REGION` をホストの `~/.aws/config` の `[default]` から確認なしに書く。** 鍵は方針に従うが、region の自動取得には方針の分岐が無い(`lib/devbase/env/collectors/aws.py` の `_collect_access_keys` の末尾)。機密の値ではないが、#314 の「確認なしに取り込まない」から外れる。 -> 2. **GCP の鍵ファイル名の正規化の警告**(`プロファイル名 '…' を … に正規化しました`)が、skip や非対話で取り込みを飛ばしたときにも出る。1 回の init で 2 回ずつ出る(表示だけ)。 -> -> ## 再現(1) -> -> 1. `$DEVBASE_ROOT/secrets/host-import.yml` に `groups: {<グループ>: skip}` を書く -> 2. そのグループのプロジェクトで端末から `devbase env init` → 方法に 3 → 鍵の 2 問は空で答える -> 3. `AWS_DEFAULT_REGION: 自動取得完了 (ap-northeast-1)` と出て書かれる -> -> `bf2b929` でも再現する(`HostImport(policy=SKIP)` で `_collect_access_keys` を直接呼び、`REGION written: ap-northeast-1`。2026-09-29)。 -> -> 方法 2(`_collect_sso_profile`、`aws.py:254-273`)も、利用者が入れたプロファイル名の region を方針によらずホストから読む。名前を利用者が明示するため害は小さい。 -> -> ## 見つけた場所 -> -> v4.0.0 のリリース後テスト(#314 の確認。一時的な DEVBASE_ROOT と偽の HOME、plaintext backend)。 -> -> ## 受け入れ条件(案) -> -> - [ ] 方針が `skip`、または `ask` で断ったとき、方法 3 でも `AWS_DEFAULT_REGION` をホストから取らない -> - [ ] 取り込みを飛ばしたときは GCP の正規化の警告を出さない -> -> - [ ] 方法 2 の region の読み取りも同じ方針に従うか、従わないならその理由を仕様に書く -> -> ## 直す場所 -> -> | 項目 | 内容 | -> | --- | --- | -> | 現れている場所 | (1)`lib/devbase/env/collectors/aws.py` の `_collect_access_keys` の末尾(`:301-309`)。(2)`lib/devbase/env/collectors/google.py:44` の `_safe_profile_name` が出す警告と、`lib/devbase/commands/env.py:963` の `_gcp_profile_files` が出す 2 回目 | -> | 直す場所 | (1)同じ `aws.py`。`host` の取り込みの方針を region の自動取得にも当てる。(2)GCP の鍵ファイルの発見を `google.py` の 1 関数にまとめ、`env._gcp_profile_files` もそれを使う。警告は純粋な `_safe_profile_name` から外し、取り込むと決めた後に 1 回だけ出す(手: 統合 consolidate_duplication) | -> -> (2)の発見の二重は、#310 の `_gcp_profile_files` の項と同じ場所である。 -> -> 関連: #314(鍵の確認は入ったが、方法 3 を明示して選んだ経路の region が確認の外に残った) #310 -> - -## 目的 - -- `env init` が、取り込みの方針(`ask` / `skip` / `import`)に従わずにホストの `~/.aws/config` の region を参照へ書くことを無くす(#314 の「確認なしに取り込まない」を AWS の方法 2・方法 3 の region にも当てる) -- 取り込みを飛ばしたときに、GCP のプロファイル名の正規化の警告を出さない。取り込むときも 1 回の `env init` で同じ警告を 2 回出さない - -## 前提 - -- 前提 1: ホストの `~/.aws/config` から読んだ region を参照の `AWS_DEFAULT_REGION` へ書くことは、用語集の「取り込み」に当たる(機密の値でなくても、ホストの資格情報ファイルから読んだ値だから)。利用者がキーボードで入れた region と、既定の値 `ap-northeast-1` を書くことは取り込みに当たらない -- 前提 2: 方法 2(SSO Profile)の region も取り込みの方針に従わせる(受け入れ条件(案)の 3 つ目は「従う」を採る)。プロファイル名を利用者が入れても、そのプロファイルの region を書くことまで同意したとは読まない -- 前提 3: 方針が `ask` のとき、ホストの region を使うかは質問を増やさずに確かめる。方法 3 で鍵の確認(「取り込みますか?」)を出したときはその答えを region にも当て、確認を出さなかったとき(ホストに鍵が無い)と方法 2 では、region の入力の既定の値としてホストの region を見せる(Enter で受け入れ、入力すればその値) -- 前提 4: 方針が `skip`(端末でない `ask` から落ちたものを含む)と、`ask` で断ったときは、ホストの `~/.aws/config` の region を読まず、今の「region が無いとき」と同じ入力(既定の値 `ap-northeast-1`)へ進む -- 前提 5: 方法 1(Config Files)の region は変えない。方法 1 の region は選んだ AWS のプロファイルの節から読み、その節は取り込みの選択に入っているため -- 前提 6: GCP の正規化の警告は、取り込むと決めた鍵の名前が正規化で変わったときに、その名前ごとに 1 回だけ出す。`env sync` と同期済みハッシュの控えの更新でプロファイル名から鍵ファイルを引くときは警告を出さない(名前は `env init` で正規化済みのため) -- 前提 7: 正規化した名前の衝突の警告(後の鍵ファイルを候補から外す)も、正規化の警告と同じ規則で出す。方針が `skip` なら出さない -- 前提 8: `env init` の取り込みの方針の決め方(#314 の `host-import.yml`・`HostImport`)は変えない - -## 対象範囲 - -含む: -- AWS の方法 3(Access Key)が region をホストから読むかを、取り込みの方針で決める -- AWS の方法 2(SSO Profile)が region をホストから読むかを、取り込みの方針で決める -- GCP のプロファイル名の正規化の警告と衝突の警告を出す時機と回数 -- GCP の鍵ファイルの発見を `env init` と `env sync` / 控えの更新で同じ 1 つの引き方にまとめること(警告を出さない引き方として) - -含まない: -- 方法 1(Config Files)の region と、`import` の方針の振る舞い(前提 5) -- 方法 2 でホストのプロファイル名の一覧を表示すること(表示であり、参照へ書かない) -- 方法 2・方法 3 で `AWS_DEFAULT_REGION` が既に参照にあるときの扱い(今のまま) -- 正規化の規則そのもの(`[A-Za-z0-9_]` 以外を `_` へ置き換える) -- #310 に並ぶ `env.py` のほかの構造改善(`_gcp_profile_files` の名前の不揃い以外) - -## ドメインイベント - -| # | イベント | 引き金 | 失敗したとき | 順序の前提 | -| --- | --- | --- | --- | --- | -| E1 | 取り込みの方針が決まった | `env init` が対象のグループと `host-import.yml` を読む | 設定が壊れていれば init を止める(#314 のまま) | — | -| E2 | AWS の認証方法が選ばれた | 利用者が 1〜4 を入れる | 読めない入力は方針の既定の方法へ落ちる(今のまま) | E1 | -| E3 | 方法 3 でホストの鍵の扱いが決まった | `[default]` に鍵がある。`import` は取り込み、`skip` は飛ばし、`ask` は確認を出す | 確認の入力が EOF なら断ったとみなす(既定 N) | E2 | -| E4 | region の出所が決まった(ホスト / 利用者の入力 / 既定の値) | 方法 2・方法 3 で `AWS_DEFAULT_REGION` が参照に無い | ホストに region が無ければ入力へ進む | 方法 3 は E3、方法 2 は E2 | -| E5 | `AWS_DEFAULT_REGION` を参照へ書いた | E4 | — | E4 | -| E6 | GCP の鍵ファイルの候補を見つけた | `~/gcp-credentials/` を読む | 衝突した名前は後の鍵ファイルを外す | E1 | -| E7 | 取り込む GCP の鍵が決まった | `import` は全部、`ask` は選んだもの、`skip` は無し | 0 件なら GCP の鍵と共通設定を書かない(今のまま) | E6 | -| E8 | 正規化と衝突の警告を出した | E7 で決めた鍵の名前が正規化で変わった、または衝突で外れた鍵がある | — | E7(E6 の時点では出さない) | -| E9 | 同期済みハッシュの控えを更新した | init の終わり、または `env sync` | — | E7。警告を出さない | - -E3 の「確認を出さなかった」(ホストに鍵が無い)と「断った」を E4 が分けて扱う点は前提 3・前提 4 に移した。E8 を E7 の後に置く順序は前提 6 に移した。 - -## 用語 - -| 用語 | 意味 | -| --- | --- | -| 取り込み | 用語集のとおり(コンテキスト secret) | -| 取り込みの方針 | 用語集のとおり | -| 取り込みの候補 | 用語集のとおり | -| 取り込みの選択 | 用語集のとおり | -| AWS のプロファイル | 用語集のとおり | -| プロファイル名の正規化 | GCP の鍵ファイル名の拡張子を除いた部分のうち `[A-Za-z0-9_]` 以外を `_` へ置き換え、GCP のプロファイル名(`GCP_CREDENTIALS_BASE64_<名前>` の `<名前>`)にすること | - -## 受け入れ条件 - -AWS の方法 3(Access Key)。いずれも参照に `AWS_DEFAULT_REGION` が無く、ホストの `~/.aws/config` の `[default]` に `region = ap-southeast-2` がある状態で始める(既定の値 `ap-northeast-1` と区別するため)。 - -- [ ] 方針が `skip` で方法 3 を選び、鍵の 2 問を空で答え、region の入力を空で答えると、参照の `AWS_DEFAULT_REGION` は `ap-northeast-1` になり、`ap-southeast-2` は書かれない -- [ ] 方針が `skip` で方法 3 を選ぶと、出力に「`AWS_DEFAULT_REGION: 自動取得完了`」が出ない -- [ ] 端末でない標準入力で方針 `ask` が `skip` へ落ちたときも、方法 3 で `ap-southeast-2` が書かれない -- [ ] 方針が `ask` でホストに鍵があり、確認に `N` と答えて region の入力を空で答えると、`AWS_DEFAULT_REGION` は `ap-northeast-1` になる -- [ ] 方針が `ask` でホストに鍵があり、確認に `y` と答えると、`AWS_DEFAULT_REGION` は `ap-southeast-2` になる(region の入力を出さない) -- [ ] 方針が `ask` でホストに鍵が無いとき、region の入力の既定の値として `ap-southeast-2` を見せ、空で答えると `ap-southeast-2`、`us-east-1` と入れると `us-east-1` が書かれる -- [ ] 方針が `import` で方法 3 を選ぶと、今と同じく `AWS_DEFAULT_REGION` は `ap-southeast-2` になり、region の入力を出さない - -AWS の方法 2(SSO Profile)。参照に `AWS_DEFAULT_REGION` が無く、ホストの `~/.aws/config` の `[profile dev]` に `region = ap-southeast-2` がある状態で、プロファイル名に `dev` と入れる。 - -- [ ] 方針が `skip` で region の入力を空で答えると、`AWS_DEFAULT_REGION` は `ap-northeast-1` になる -- [ ] 方針が `ask` のとき、region の入力の既定の値として `ap-southeast-2` を見せ、空で答えると `ap-southeast-2` が書かれる -- [ ] 方針が `import` なら、今と同じく `AWS_DEFAULT_REGION` は `ap-southeast-2` になり、region の入力を出さない - -GCP のプロファイル名の正規化。`~/gcp-credentials/` に `my-proj.json`(正規化で `my_proj` になる)と `plain.json` がある状態で `env init` を 1 回走らせる。 - -- [ ] 方針が `skip` なら、出力に「プロファイル名 'my-proj' を 'my_proj' に正規化しました」が 1 度も出ない -- [ ] 方針が `ask` で何も選ばずに断ると、正規化の警告が 1 度も出ない -- [ ] 方針が `ask` で `plain` だけを選ぶと、正規化の警告が 1 度も出ない -- [ ] 方針が `ask` で `my_proj` を選ぶと、正規化の警告がちょうど 1 回出る -- [ ] 方針が `import` なら、正規化の警告がちょうど 1 回出る -- [ ] `~/gcp-credentials/` に `a-b.json` と `a_b.json` があり方針が `skip` なら、衝突の警告が出ない。方針が `import` なら衝突の警告がちょうど 1 回出る -- [ ] 鍵ファイルが正規化した名前で参照にある状態で `env sync` を走らせると、正規化の警告が出ず、`GCP_CREDENTIALS_BASE64_my_proj` の鍵ファイルを今と同じく `my-proj.json` として引く - -退行しないこと: - -- [ ] AWS の方法 3 で、#314 の鍵の扱い(`skip` で鍵を書かない・`ask` で断ると鍵を書かない・`import` で鍵を書く)が変わらない -- [ ] 方法 3 で `AWS_DEFAULT_REGION` が既に参照にあれば、今と同じくその値を残し、ホストを読まない(方法 2 は方針に従って決め直す。設計の承認で決めた) -- [ ] 方法 1 の region の振る舞いと出力が変わらない -- [ ] GCP の取り込みで参照へ書くキーと値(`GCP_CREDENTIALS_BASE64_<名前>`・`GCP_ACTIVE_PROFILE` ほか)が、どの方針でも今と同じになる -- [ ] 既存のテスト(`tests/env/test_host_import.py`・`tests/env/test_collector_host.py`・`tests/commands/test_env_host_import.py` ほか)がすべて通る - -## 非機能の条件 - -| 大項目 | 条件 | -| --- | --- | -| セキュリティ | 方針が `skip`(端末でない `ask` を含む)のとき、方法 2・方法 3 は `~/.aws/config` の region を参照へ書かない。テストは偽の HOME に置いた `~/.aws/config` で確かめ、利用者の実際の `~/.aws` を読まない | -| 運用・保守性 | region をホストから取らなかったときは、今の `skip` の知らせ(`取り込まない設定のため飛ばしました` / `標準入力が端末でないため飛ばしました`)と同じ形の 1 行で分かる。方法 3 で鍵と region の両方を飛ばしても、この知らせは 1 回の選択で 1 行にとどめる。新しい警告の種類は足さない | - -## 影響 - -| 対象 | 影響 | -| --- | --- | -| 公開インタフェース | コマンドとオプションは変わらない。方針が `skip` / `ask` の方法 2・方法 3 で、region の入力が出るか・その既定の値が変わる | -| データ | 参照のキーの形、同期済みハッシュの控えの形は変わらない。移行は無い | -| 既存の振る舞い | `skip` / `ask` の方法 2・方法 3 の region。GCP の正規化と衝突の警告の時機と回数 | - -## 検証手段 - -| 項目 | 手段 | -| --- | --- | -| テスト | `uv run pytest tests/env tests/commands -q`(偽の HOME と一時的な `DEVBASE_ROOT`、plaintext backend。端末かどうかは `host_import.resolve(..., interactive=)` で与える) | -| 静的解析 | `uv run ruff check lib tests` と `python -m py_compile` の CI の検査ジョブ | -| 手動確認 | 端末で、一時的な `DEVBASE_ROOT` と偽の HOME(`~/.aws/config` に region、`~/gcp-credentials/my-proj.json`)、`host-import.yml` に `groups: {<グループ>: skip}` を置き、`devbase env init` で方法 3 を選んで鍵と region を空で答える。`ap-northeast-1` が書かれ、GCP の正規化の警告が出ないことを見る(再現(1)の逆)。実際の利用者の環境では行わない | - -## 前提とする取り決め - -| 項目 | 参照先 / 決めたこと | -| --- | --- | -| プロジェクト構造 | 取り込みの方針の判断は collector の外(`host_import`)で決め、collector は `HostImport` に従う(`lib/devbase/env/host_import.py` の I1)。GCP の鍵ファイルの発見は `lib/devbase/env/collectors/google.py` の 1 か所に置き、`commands/env.py` はそれを使う | -| コーディング規約 | `CONTRIBUTING.md` と ruff の設定(`pyproject.toml`)。ログは `devbase.log` の logger | -| テスト戦略 | collector の単体テスト(`tests/env/`)で方針ごとの書き込みと出力を、コマンドの層のテスト(`tests/commands/test_env_host_import.py`)で 1 回の `env init` の警告の回数を確かめる | - -## 境界 - -| 区分 | 内容 | -| --- | --- | -| 常に行う | 偽の HOME でのテスト、ruff、既存テストの実行、CHANGELOG への利用者に見える変更の記入 | -| 確認してから行う | 方針・`host-import.yml` の形を変えること、参照のキーの形を変えること | -| 行わない | 利用者の実際の `~/.aws` / `~/gcp-credentials` を読むテスト、実際の利用者の環境での `env init`、#310 のほかの項目の構造改善 | - -## 未決 - -| 項目 | 誰が決めるか | 期限 | -| --- | --- | --- | -| 方法 2 の region を方針に従わせる(前提 2)のを採るか。従わせない場合は理由を仕様に書く | 設計 PR のレビューで利用者 | 設計 PR のマージまで | -| `ask` でホストの region を入力の既定の値として見せる(前提 3)か、y/N の確認を 1 問足すか | 設計(`design`)で決め、設計 PR のレビューで利用者が確かめる | 設計 PR のマージまで | diff --git a/issues/issue-357-design-decisions.md b/issues/issue-357-design-decisions.md deleted file mode 100644 index 3426ab17..00000000 --- a/issues/issue-357-design-decisions.md +++ /dev/null @@ -1,195 +0,0 @@ -# #357: 決定の記録 - -[issue-357-design.md](issue-357-design.md)(#357 の設計)の決定と、その根拠にした実測を持つ。 -不変条件(I1〜I9)と受け入れ条件の番号は、設計と要求([issue-357-requirements.md](issue-357-requirements.md))のものを指す。 - -## 決定の記録 - -### 決定 1: リンクの段のロックと張り替えの試行の両方を入れる - -ロックだけでは、修正前の手順で張る相手(ロックを取らない)と重なったときに落ちるため、受け入れ条件 7 を -満たせない。張り替えの試行だけでは、共有のリンクは正しくなるが、退避とプレースホルダが重なる経路が残り、 -対象範囲の「リンクの段の全体を同時に走らせた結果が、1 つずつ順に走らせた結果と同じ」を、処理ごとの場合分けで -示すことになる。両方を入れると、修正後どうしは順に走るため結果が同じになり、ロックが効かない相手と場面は -張り替えの試行が受ける。ロックを取れないときの退避とプレースホルダは、排他の作成が受ける(決定 10)。 - -根拠: Mission / Vision(MVV 版 1) - -### 決定 2: ロックは共通のボリュームのルートのディレクトリそのものに掛け、ロックのためのファイルを作らない - -退避は共通のボリュームへ書き、共通のボリュームはすべてのコンテナが共有する。ここに 1 つだけロックを置けば、 -グループのボリュームの処理も含めてリンクの段の全体が順に走る。ディレクトリそのものに掛けるため、ボリュームに -残るものが無く、修正前の手順とスナップショットから見える並びが変わらない(I8)。グループのボリュームに -ロックのファイルを置く方式は、退避の重なりを防げず、ボリュームにファイルが残るため採らなかった。別の -グループのコンテナも順番を待つが、リンクの段 1 回は base イメージのコンテナの中で 0.1 秒ほどである。 - -根拠: Vision(MVV 版 1) - -### 決定 3: ロックは 10 秒まで待ち、取れなければ警告を出してロックなしで進む - -持ち主が kill されたときはカーネルがすぐにロックを外すが、止まったまま終わらない持ち主がいると、上限なしでは -ホストのすべてのコンテナの起動がそこで止まる。ロックなしで進んでも、共有のリンクは張り替えの試行で正しく -なり、退避とプレースホルダは排他の作成で 1 つのプロセスだけが書く(決定 10)。失うのは、リンクの段の全体が -1 つずつ順に走る保証だけである。待ち切れないときに非 0 で終わる方式は、1 つのコンテナの不調をほかのコンテナの起動の失敗へ広げるため採らなかった。10 秒は、8 個が順に -走る所要(実測で 0.75〜1.9 秒)より長く、非機能の条件の 30 秒より短い値として置く。 - -根拠: Mission(MVV 版 1) - -### 決定 4: 張り替えは「消す → `ln -sn` → 読み直す」の繰り返しにし、一時的な名前と置き換え(rename)を使わない - -一時的な名前で張ってから `mv -T` で置き換える方式は、位置が実体のディレクトリのときに置き換えられず、結局 -消す手順が要る。`mv -T` は macOS の `mv` に無く、既存の 35 件が開発者の端末で走らなくなる。途中で kill されると -一時的な名前がボリュームに残る。読み直しで判定する方式は、base イメージと macOS の両方にあるコマンドだけで -書け、ボリュームに余分な名前を作らない(I8)。置き換えの利点である「リンクが無い瞬間が無い」は、修正後 -どうしではロックが与え、修正前の手順の相手に対しては相手の側が消すため、どちらの方式でも得られない。 - -根拠: P1(MVV 版 1) - -### 決定 5: ロックはサブシェルの中だけで持つ - -リンクの段の関数は、サブシェルを開き、そのサブシェルにだけ付けた fd で共通のボリュームのルートを開いて -ロックを掛け、同じサブシェルの中で本体を呼ぶ。サブシェルを抜ければ、成功でも失敗でも fd が閉じる。 -呼び出したシェルで fd を開く方式は、閉じ忘れると entrypoint が最後に `exec` するコマンドへ fd が引き継がれ、 -コンテナが動いている間ロックを持ち続けるため採らなかった(I7)。 - -根拠: Mission(MVV 版 1) - -### 決定 6: 張る操作に `-n` を付ける - -修正前の手順の `ln -s` は、位置が既にディレクトリを指すリンクになっていると、失敗せずに、その先のディレクトリの -中へリンクを作って 0 で終わる。同時起動で相手が先に張ったときにこの形になり、共通のボリュームの `plugins` の -中に `plugins` という入れ子のリンクが残る(下の実測)。`-n` を付けると、位置そのものを対象にして失敗し、 -読み直しへ進む。 - -根拠: Value 2(MVV 版 1) - -### 決定 7: `flock` が無い環境ではロックなしで進み、同時実行のテストを skip しない - -base イメージには `flock` が必ずあるが、開発者の macOS には無い。無いときに止めると、既存の 35 件が macOS で -走らなくなる。ロックなしでも、張り替えの試行と排他の作成(決定 10)で受け入れ条件 1〜7 を満たすため -(下の実測)、同時実行のテストは macOS でも走らせる。理由を示して skip するのは、ロックそのものを確かめるテスト(I5・I7 と、非機能の -可用性)と、書き込めない状態を作れない root での受け入れ条件 9、Docker かイメージが無いときの受け入れ条件 13 -だけである。要求の未決「macOS で走れないテストが出た場合に skip でよいか」は、この範囲の skip として決める。 - -根拠: 根拠なし(MVV 版 1) - -### 決定 8: 張り替えの試行は、ホームのリンクも共有のリンクも同じ関数で行う - -同時起動で重なるのは共有のリンクだけだが、リンクを張る関数を 2 つに分けると、位置に実体があるときの扱い -(要求の前提 3)を 2 か所で保つことになる。ホームのリンクでは試行が 1 回で終わり、振る舞いは今と変わらない。 - -根拠: 根拠なし(MVV 版 1) - -### 決定 9: 実物のコンテナでの確認は、イメージを建て直さず、作業ツリーの `entrypoint.sh` をマウントして行う - -受け入れ条件 13 は、今ある base イメージ(`devbase-base:latest`)のコンテナに、作業ツリーの `entrypoint.sh` を -読み取り専用でマウントし、使い捨ての名前のボリューム 2 つで 8 個を同時に起動して確かめる。利用者の端末の -イメージのタグを置き換えず、利用者のボリュームにも `devbase up` にも触れない。pytest のテストとして置き、 -Docker かイメージが無いときは、base イメージを使う既存のテストと同じ決まりで skip する(CI では skip になり、 -完了判定のときに開発者の端末で走る)。イメージを建て直して確かめる方式は、確認のたびに利用者の手元の -イメージが入れ替わるため採らなかった。 - -根拠: Value 3 / P3(MVV 版 1) - -### 決定 10: 退避とプレースホルダは、排他の作成で作れた 1 つのプロセスだけが書く - -ロックなしで進むリンクの段(待ち時間の上限を超えた・`flock` が無い)は、退避を同時に始める。今の退避は -「コピー先が無ければ、ディレクトリを作って子を `cp -a` でコピーする」で、2 つが重なると、後から来た `cp -a` が -既にある子のディレクトリの中へ重ねてコピーし(`plugins/<名前>/<名前>`)、`cp` が `File exists` で失敗して -起動が止まる回もある(下の実測)。張り替えの試行だけでは、この経路が残る。対象範囲の「リンクの段の全体を -同時に走らせた結果が、1 つずつ順に走らせた結果と同じ」をロックを取れないときにも満たすため、エントリを作る -操作そのものを排他にし、作れたプロセスだけが中身を書く(I9)。 - -通常のファイルは bash の noclobber(`set -C` の `>`)で作り、ディレクトリは `perl` から `mkdir(2)` を呼んで作る。 -base イメージの `mkdir`(uutils coreutils 0.8.0)は、同時に呼ぶと 2 個以上が 0 で終わるため、作れたプロセスを -1 つに決められない(下の実測)。`perl` は base イメージに今入っており(Essential のパッケージ perl-base)、 -道具を足さない(前提 5)。`perl` が無い環境では `mkdir` で作り、止めない(決定 7 と同じ考え)。 - -採らなかった方式は次のとおりである。 - -- 一時的な名前へコピーしてから置き換える: kill で一時的な名前がボリュームに残る(I8・決定 4) -- ロックのためのファイルをエントリの隣に置く: kill で残ると、次の起動から退避が行われなくなる(I8) -- ロックを取れないときは非 0 で終わる: 1 つのコンテナの不調を、ほかのコンテナの起動の失敗へ広げる(決定 3) -- ロックを取れないときは退避を飛ばす: 続くリンクの張り替えがホームの `~/.claude` を消すため、イメージの設定を失う -- `gnumkdir`(GNU coreutils の `mkdir`)を呼ぶ: base イメージでは排他だが、CI の runner と macOS に無い名前で、 - 環境ごとの分岐が要る -- `python3` から `mkdir(2)` を呼ぶ: base イメージでは排他だが、python3 は Essential のパッケージではない - -根拠: Value 2 / Vision(MVV 版 1) - -## 決定の根拠にした実測 - -2026-10-02 に、設計のための試作(変更後の 3 つの関数を今の `entrypoint.sh` へ差し込んだもの)で測った。 -「同時に走らせる」は、別々のホームと同じ 2 つのボリュームを渡したリンクの段を、同時に始めることを指す。 -Linux は `devbase-base:latest` の使い捨てのコンテナの中、macOS は `flock` の無い開発者の端末である。 - -| 手順 | 環境 | 始めの状態 | 同時の数 × 回数 | 非 0 があった回 | 入れ子のリンクができた回 | -| --- | --- | --- | --- | --- | --- | -| 修正前 | Linux | `settings.json` だけ通常のファイル | 8 × 10 | 3 | 0 | -| 修正前 | Linux | `plugins` が実体のディレクトリ | 8 × 10 | 10 | 9 | -| 修正前 | macOS | `settings.json` だけ通常のファイル | 8 × 20 | 20 | 0 | -| 修正前 | macOS | `plugins` が実体のディレクトリ | 8 × 20 | 20 | 20 | -| 試作(ロックあり) | Linux | 受け入れ条件 1・3・4・5・6 の 6 つの状態 | 各 8 × 10 | 0 | 0 | -| 試作(ロックを外す) | Linux | `settings.json` の状態と `plugins` の状態 | 各 8 × 10 | 0 | 0 | -| 試作(`flock` なし) | macOS | 受け入れ条件 1・3・4・5・6 の 6 つの状態 | 各 8 × 20 | 0 | 0 | -| 試作 1 個と修正前 1 個 | Linux | `settings.json` の状態 | 2 × 10 | 試作の側は 0 | 0 | -| 試作 1 個と修正前 1 個 | Linux | `plugins` の状態 | 2 × 10 | 試作の側は 0 | 2(修正前の側が作った) | - -| 測ったこと | 結果 | -| --- | --- | -| 8 個同時の所要(Linux) | ロックありで 0.75〜1.9 秒、ロックを外して 0.3 秒 | -| 1 個だけの所要(Linux・張り直しなし・名前付きボリューム) | 修正前 66 ミリ秒、試作 102 ミリ秒 | -| ロックを持ったプロセスが止まったまま(待ち時間の上限を 2 秒にして測った) | 2.1 秒で終了コード 0、警告の行が 1 行 | -| ロックを持ったプロセスを 1 秒後に kill | 1.4 秒で終了コード 0、警告の行なし | -| リンクの段を抜けた後に、共通のボリュームのルートを開いた fd が残るか | 残らない(0 個) | -| 名前付きボリュームのルートへのロックと、走らせた後のルートの並び | ロックは取れる。並びは修正前と同じで、足されたエントリは無い | -| グループのボリュームの `.claude` に書き込めない状態 | 終了コード 1、`ERROR: symlink を張れない: <位置> -> <先> (ln: Permission denied)` | -| 既存の 35 件(試作を差し込んだ `entrypoint.sh` へ向けた。macOS) | 35 件とも通る | - -base イメージの `ln`(uutils coreutils 0.8.0)は、次のとおり振る舞う。 - -```text -$ mkdir -p /tmp/t/real; ln -s /tmp/t/real /tmp/t/link -$ ln -s /tmp/t/real /tmp/t/link; echo "rc=$?"; ls /tmp/t/real -rc=0 -real -$ ln -sn /tmp/t/real /tmp/t/link; echo "rc=$?" -ln: Already exists -rc=1 -``` - -決定 10 のための実測は、2026-10-02 に、同じ 2 つの環境で行った。始めの状態は、空の 2 つのボリュームと、 -どのホームにも同じ中身の `~/.claude`(`plugins` の下に子のディレクトリ 12 個・`skills`・`commands`・`CLAUDE.md`・ -`settings.json`)である。ロックは入れずに 8 個を同時に始め、終わった後の 2 つのボリュームの並び・リンクの先・ -ファイルの中身を、1 個だけ走らせた後と比べた。「張り替えの試行」と「排他の作成」は、設計のとおりに書いた -関数を今の `entrypoint.sh` へ読み込ませたものである。 - -| 走らせたもの | 手順 | 環境 | 回数 | 非 0 があった回 | 1 個だけの結果と違った回 | -| --- | --- | --- | --- | --- | --- | -| 退避だけ | 今の退避 | Linux | 20 | 20 | 20(20 回とも重ねたコピーが残った) | -| 退避だけ | 今の退避 | macOS | 20 | 17 | 20(20 回とも重ねたコピーが残った) | -| 退避だけ | 排他の作成(ディレクトリを `mkdir` で作る) | Linux | 20 | 0 | 4 | -| 退避だけ | 排他の作成(ディレクトリを `perl` の `mkdir` で作る) | Linux | 50 | 0 | 0 | -| 退避だけ | 排他の作成(同上) | macOS | 50 | 0 | 0 | -| リンクの段の全体 | 修正前 | Linux | 20 | 20 | 20 | -| リンクの段の全体 | 修正前 | macOS | 20 | 20 | 20 | -| リンクの段の全体 | 張り替えの試行だけ | Linux | 20 | 20 | 20 | -| リンクの段の全体 | 張り替えの試行だけ | macOS | 20 | 13 | 20 | -| リンクの段の全体 | 張り替えの試行と排他の作成 | Linux | 50 | 0 | 0 | -| リンクの段の全体 | 張り替えの試行と排他の作成 | macOS | 50 | 0 | 0 | - -無いエントリを 8 個が同時に作ったときに、2 個以上が終了コード 0 で終わった回は、次のとおりである(各 200 回)。 - -| 作り方 | Linux(base イメージ) | macOS | -| --- | --- | --- | -| `mkdir`(Linux は uutils coreutils 0.8.0) | 122 回・180 回(2 度測った) | 0 回 | -| bash の noclobber(`set -C` の `>`) | 0 回 | 0 回 | -| `perl -e 'mkdir(...)'`(Linux は 5.40.1、macOS は 5.34.1) | 0 回 | 0 回 | -| `gnumkdir`(GNU coreutils 9.7) | 0 回 | 無い | -| `python3` の `os.mkdir` | 0 回 | 0 回 | - -| 測ったこと | 結果 | -| --- | --- | -| `perl` の `mkdir` を 20 回続けて呼んだ所要(Linux) | 13 ミリ秒 | -| 既存の 35 件(排他の作成を差し込んだ `entrypoint.sh` へ向けた。macOS) | 35 件とも通る | -| base イメージの `perl` のパッケージ | perl-base(Essential: yes・Priority: required)。python3-minimal は Essential ではない | diff --git a/issues/issue-357-design.md b/issues/issue-357-design.md deleted file mode 100644 index 73353e42..00000000 --- a/issues/issue-357-design.md +++ /dev/null @@ -1,441 +0,0 @@ -# #357: 同時起動でもリンクの段が失敗しない張り方 - -要求と受け入れ条件は #357 の本文にある(コピーは [issue-357-requirements.md](issue-357-requirements.md))。 -この文書は「どう作るか」だけを扱う。決定の理由は [issue-357-design-decisions.md](issue-357-design-decisions.md) に分けた。 - -変えるのは `containers/base/entrypoint.sh` のリンクの段だけで、次の 3 つを入れる。 - -- **リンクの段のロック**: 修正後のリンクの段どうしを、1 つずつ順に走らせる -- **張り替えの試行**: リンクを張る操作の成否ではなく、張った後に読み直した結果で成功を決める。修正前の手順で - 張る相手と重なっても、ロックが取れなくても、失敗しない -- **排他の作成**: 退避とプレースホルダは、エントリを「無いときだけ作る」操作で作れた 1 つのプロセスだけが - 中身を書く。ロックが取れなくても、同じエントリへ 2 つのプロセスが書かない - -## ドメインモデル - -### コンテキスト - -| コンテキスト | 何の語が 1 つの意味に決まるか | -| --- | --- | -| コンテナの起動(`container-start`) | 共通のボリューム・共有のリンク・リンクの段・同時起動・修正前の手順と、この設計で足すリンクの段のロック・ロックの待ち時間の上限・張り替えの試行・入れ子のリンク・排他の作成 | - -変更は 1 つのコンテキストで閉じる。アカウントグループとグループのボリュームは「機密の置き場」(`secret`)の語を -そのままの意味で使う(順応者)。コンテナを起動する側(`devbase up` / `devbase scale`)には触れない。 - -### 集約 - -| 集約 | 持ち主(書き換えてよいもの) | 根 | エンティティ | 値オブジェクト | -| --- | --- | --- | --- | --- | -| 共有のリンク | リンクを張る関数(`devbase_link_setting`) | 共有のリンク(グループのボリュームの `.claude` の下の位置で識別する) | — | リンクの先(パスの文字列)・位置の状態(無い / 正しいリンク / 別の先を指すリンク / 実体) | -| リンクの段 | リンクの段の関数(`devbase_setup_ai_settings`) | リンクの段の 1 回の実行 | 張り替えの試行(リンクごとの何回目かで識別する) | リンクの段のロックの結果(取れた / 待ち時間の上限を超えた / `flock` が無い)・排他の作成の結果(作れた / 既にあった / 作れない)・ロックの待ち時間の上限・試行の上限 | - -共有のリンクの持ち主は関数として 1 つだが、同じグループのコンテナの数だけ別々のプロセスで動く。この集約の -不変条件は、ほかのプロセスが同じ位置を書き換えることを前提に書く。修正前のイメージのコンテナも同じ位置を -書くが、その手順は変えられない(要求の前提 1)。 - -### 不変条件 - -| # | 集約 | 条件 | 破れたときの扱い | -| --- | --- | --- | --- | -| I1 | 共有のリンク | リンクを張る関数が終了コード 0 で返るのは、リンクの位置が正しい先を指す symlink であると読み直して確かめたときだけである。張る操作(`ln`)と消す操作(`rm`)の終了コードでは決めない | 同時起動で落ちる、または正しくないリンクのまま起動が進む。テストが落とす | -| I2 | 共有のリンク | 張る操作は、リンクの位置がディレクトリを指すリンクになっていても、その先のディレクトリの中へリンクを作らない(入れ子のリンクを作らない) | 共通のボリュームのディレクトリの中に、自分自身を指すリンクが残る。テストが落とす | -| I3 | 共有のリンク | 試行の上限(20 回)まで繰り返しても正しくならなければ、非 0 で返り、リンクの位置のパスを含む行を標準エラーへ出す | 張れないリンクを成功として扱う、または失敗の場所が分からない。テストが落とす | -| I4 | 共有のリンク | 終了コード 0 で返るとき、`ln:` で始まる行を出さない | 成功した起動のログに失敗の行が残り、利用者が失敗と読む。テストが落とす | -| I5 | リンクの段 | リンクの段のロックを取れた修正後のリンクの段どうしは、退避・プレースホルダ・リンクの処理が時間の上で重ならない | 退避とプレースホルダが重なり、1 個ずつ順に走らせた結果と違う状態が残る。テストが落とす | -| I6 | リンクの段 | リンクの段のロックを取れない(待ち時間の上限を超えた・`flock` が無い)ときは、止まらずに進み、I1〜I4 と I9 を守る | 1 つのコンテナが止まっただけで、ほかのコンテナの起動が止まる。テストが落とす | -| I7 | リンクの段 | リンクの段のロックは、リンクの段を抜けた後(成功でも失敗でも)に残らない。entrypoint が最後に起動するコマンドへ引き継がない | そのコンテナが動いている間、ほかのすべてのコンテナのリンクの段が待ち時間の上限まで待つ。テストが落とす | -| I8 | リンクの段 | 2 つのボリュームに、ロックのためのファイルも、一時的な名前のエントリも作らない | 修正前の手順や復元したボリュームに見慣れないエントリが残る。テストが落とす | -| I9 | リンクの段 | 退避とプレースホルダが 2 つのボリュームに作るエントリは、排他の作成で作れた 1 つのプロセスだけが中身を書く。作れなかったプロセスは、そのエントリを「既にある」として扱い、書かない。リンクの段のロックを取れたかどうかによらない | 2 つの退避が同じディレクトリへ重ねてコピーし、子のディレクトリの中に同じ名前のディレクトリがもう 1 つ残る(例: 共通のボリュームの `plugins/<名前>/<名前>`)。または `cp` が失敗して起動が止まる。テストが落とす | - -I9 から、どのホームにも同じ中身があるとき(受け入れ条件 3 の前提)は、ロックを取れなくても、退避とプレースホルダが -残すデータが 1 個ずつ順に走らせた結果と同じになる。ホームの中身が違うときの扱いは「未確認のまま残ること」に書いた。 - -### ドメインイベント - -要求の E1〜E7 を引き継ぎ、E8 を足す。起きる順は E1 → E2 → E8 → E3 →(リンクごとに E4 か E5)→ E6 である。 - -| # | イベント | 発生元 | 受け手 | -| --- | --- | --- | --- | -| E1 | リンクの段が始まった | entrypoint の本文(コンテナの起動) | リンクの段の関数 | -| E2 | 2 つのボリュームのルートを用意した | リンクの段の関数 | リンクの段の関数(ロックを取りに行く) | -| E8 | リンクの段のロックを取った、または取らずに進むと決めた | リンクの段の関数 | リンクの段の本体 | -| E3 | イメージの設定を共通のボリュームへ退避し、プレースホルダを作った | リンクの段の本体 | リンクの段の本体(リンクへ進む) | -| E4 | 共有のリンクが正しいと確かめた | リンクを張る関数 | リンクの段の本体(次のリンクへ進む) | -| E5 | 共有のリンクを張り直した | リンクを張る関数 | 同じグループのほかのコンテナのリンクの段(E4 として観測する) | -| E6 | リンクの段が終わった | リンクの段の関数 | entrypoint の本文(続きの処理へ進む)と、ロックを待っていたほかのコンテナのリンクの段 | -| E7 | 共有のリンクが通常のファイルへ置き換わった | 分かっていない(#372) | 次の起動のリンクの段(E5 の引き金になる) | - -### 用語 - -| 用語 | 意味 | 用語集への反映 | -| --- | --- | --- | -| 共通のボリューム | すべてのコンテナが `/persistent/ai` にマウントする 1 つのボリューム | 追加済み(`container-start`、要求の段階) | -| 共有のリンク | グループのボリュームの `.claude` の下にあり、共通のボリュームの同じ名前のエントリを指す symlink の 5 本 | 追加済み(`container-start`、要求の段階) | -| リンクの段 | entrypoint が AI 設定のリンクを張る処理の 1 回の実行 | 追加済み(`container-start`、要求の段階) | -| 同時起動 | 2 つ以上のコンテナのリンクの段が、時間の上で重なって走ること | 追加済み(`container-start`、要求の段階) | -| 修正前の手順 | #357 より前のリンクの張り方 | 追加済み(`container-start`、要求の段階) | -| リンクの段のロック | 共通のボリュームのルートのディレクトリに対して取る排他のロック。修正後のリンクの段どうしを 1 つずつ順に走らせる | 追加(`container-start`) | -| ロックの待ち時間の上限 | リンクの段のロックを待つ秒数の上限(10 秒)。超えたらロックなしで進む | 追加(`container-start`) | -| 張り替えの試行 | 共有のリンクを「消す → 張る → 読み直して確かめる」1 回ぶん。正しくなるまで試行の上限(20 回)まで繰り返す | 追加(`container-start`) | -| 入れ子のリンク | リンクの位置がディレクトリを指すリンクになっているときに、張る操作がその先のディレクトリの中へ作るリンク(例: 共通のボリュームの `plugins/plugins`) | 追加(`container-start`) | -| 排他の作成 | エントリを「無いときだけ作る」操作。同時に呼んでも作れるのは 1 つのプロセスだけで、作れたプロセスだけがそのエントリの中身を書く | 追加(`container-start`) | - -## 機能一覧 - -| # | 機能 | 誰が使うか | -| --- | --- | --- | -| F1 | ボリュームを共有するコンテナを同時に起動しても、すべてのコンテナが起動する | devbase の利用者(`devbase up` / `devbase scale` / ホストの再起動の後の `docker start`) | -| F2 | 修正前のイメージのコンテナと並んで起動しても、修正後のコンテナが起動する | イメージを順に建て直している利用者 | -| F3 | リンクを張れないときに、どのリンクが張れなかったかを起動ログで知る | devbase の利用者 | - -## 構成要素 - -| 要素 | 責務 | 新設 / 変更 | -| --- | --- | --- | -| リンクの段の関数(`devbase_setup_ai_settings`) | 2 つのボリュームのルートを用意し、リンクの段のロックを取って本体を呼ぶ。本体の終了コードをそのまま返す。引数の形は変えない | 変更 | -| リンクの段の本体(`devbase_apply_ai_settings`) | 退避・ホームのリンク・共有のリンクを、今と同じ順で行う | 新設(リンクの段の関数から切り出す) | -| リンクを張る関数(`devbase_link_setting`) | リンクの先のプレースホルダを用意し、張り替えの試行を繰り返す。引数の形と、成功のときの起動ログの行の形は変えない | 変更 | -| 退避の 1 エントリ(`devbase_seed_entry`) | コピー先のエントリを排他の作成で作り、作れたときだけ中身をコピーする。引数の形と起動ログの行の形は変えない | 変更 | -| プレースホルダの作成(`devbase_ensure_entry`) | 無いエントリを排他の作成で作り、作れたときだけ中身(JSON のファイルは `{}`)を書く。引数の形は変えない | 変更 | -| 排他の作成(`devbase_create_entry`) | エントリを無いときだけ作り、結果を終了コード(作れた 0 / 既にあった 1 / 作れない 2)で返す。通常のファイルは bash の noclobber で、ディレクトリは `perl` から `mkdir(2)` を呼んで作る(決定 10) | 新設 | -| リンクの判定(`devbase_link_is_set`) | リンクの位置が、渡した先を指す symlink かを終了コード(0 / 1)で返す。何も出力しない | 新設 | -| 上限の定数(`DEVBASE_LINK_LOCK_WAIT`・`DEVBASE_LINK_TRIES`) | ロックの待ち時間の上限(10 秒)と試行の上限(20 回)を、ほかの一覧の定数と同じ場所に持つ | 新設 | -| 同時実行のテスト(`tests/containers/test_entrypoint_ai_settings_concurrent.py`) | Docker を使わずに、受け入れ条件 1〜7・9〜11 と I1〜I9、非機能の条件を縛る | 新設 | -| 実物のイメージのテスト(`tests/containers/test_base_image_link_stage.py`) | base イメージのコンテナ 8 個を使い捨てのボリュームで同時に起動し、受け入れ条件 13 を縛る。base イメージの道具での I9 も縛る | 新設 | -| 変更履歴(`CHANGELOG.md`) | `[Unreleased]` の `Fixed` に、同時起動で落ちなくなったことと、反映にイメージの建て直しが要ることを書く | 変更 | -| 用語集(`docs/glossary/glossary.json` と `docs/glossary.md`) | この設計で足す 5 語を `container-start` に持つ | 変更(設計の Pull Request で行う) | - -変えない関数は、ボリュームのルートの用意(`devbase_ensure_persistent_root`)と、退避するエントリを選ぶ関数 -(`devbase_seed_image_claude_settings`)の 2 つである。既存のテスト -(`tests/containers/test_entrypoint_ai_settings.py`)も変えない(受け入れ条件 12)。 - -変える 4 つの関数の呼び出し元は次のとおりで、どれも引数の形を変えないため直さない。 - -| 関数 | 呼び出し元 | -| --- | --- | -| リンクの段の関数 | entrypoint の本文の 1 か所と、既存のテスト | -| リンクを張る関数 | リンクの段の本体の 3 つのループ(分類 A のホームのリンク・分類 B のホームのリンク・共有のリンク)。`docs/specifications/shellrc-dir.md` はこの関数が `~/.shellrc.d` を張ると書いており、記述は変更後も成り立つ | -| 退避の 1 エントリ | 退避するエントリを選ぶ関数(共通のボリュームへ)と、リンクの段の本体(Kiro CLI の認証の状態を、初回だけホームからグループのボリュームへ取り込むループ)。どちらも排他の作成を通る | -| プレースホルダの作成 | リンクを張る関数と、既存のテスト。`docs/specifications/shellrc-dir.md` は、置き場所の実体をこの関数が作り、開発ユーザーの所有になると書いており、記述は変更後も成り立つ | - -### 文脈 - -```mermaid -graph TD - U[利用者] -->|devbase up・scale
docker start| D[Docker] - D -->|同時に起動する| N[修正後のイメージの
コンテナ] - D -->|同時に起動する| O[修正前のイメージの
コンテナ] - N -->|リンクの段| V[共通のボリュームと
グループのボリューム] - O -->|修正前の手順| V -``` - -変えられない外部は、Docker と、修正前のイメージのコンテナである。 - -### 構成要素の関係 - -```mermaid -graph TD - subgraph entrypoint - M[entrypoint の本文] - S[リンクの段の関数] - A[リンクの段の本体] - L[リンクを張る関数] - C[リンクの判定] - K[上限の定数] - E[退避の 1 エントリ] - P[プレースホルダの作成] - R[排他の作成] - X[変えない 2 つの関数] - end - subgraph テスト - T1[同時実行のテスト] - T2[実物のイメージのテスト] - T0[既存のテスト] - end - M -->|呼ぶ| S - S -->|ロックを取って呼ぶ| A - S -->|ルートの用意| X - A -->|退避するエントリを選ぶ| X - X -->|エントリごとに呼ぶ| E - A -->|グループのボリュームへ取り込む| E - A -->|リンクごとに呼ぶ| L - L -->|プレースホルダ| P - E -->|無いときだけ作る| R - P -->|無いときだけ作る| R - L -->|読み直す| C - S -->|待ち時間の上限| K - L -->|試行の上限| K - T1 -->|関数を読み込んで呼ぶ| S - T0 -->|関数を読み込んで呼ぶ| S - T2 -->|コンテナとして起動する| M -``` - -変更履歴と用語集は、ほかの要素と呼び出しの関係を持たない。 - -### 配置 - -```mermaid -graph TD - subgraph コンテナ A・修正後 - A1[リンクの段] - end - subgraph コンテナ B・修正後 - B1[リンクの段] - end - subgraph コンテナ C・修正前 - C1[修正前の手順] - end - subgraph Docker のボリューム - AI[共通のボリューム
/persistent/ai] - G[グループのボリューム
/persistent/group] - end - A1 -->|ロック・退避・プレースホルダ| AI - B1 -->|ロック・退避・プレースホルダ| AI - A1 -->|共有のリンク| G - B1 -->|共有のリンク| G - C1 -->|ロックを取らずに書く| G -``` - -コンテナの境界をまたぐのは、2 つのボリュームへの読み書きと、共通のボリュームのルートに掛けるロックである。 -ロックはカーネルがプロセスの開いたファイルごとに持ち、ボリュームには何も書かない(I8)。排他の作成は、 -ボリュームのファイルシステムが「既にあれば失敗する」作成を 1 つのプロセスにだけ通すことに頼り、作るのは -退避とプレースホルダが今も作っているエントリだけである。配置そのものは変えない。 - -### 置き場所 - -```text -containers/base/ -└── entrypoint.sh # 関数 4 つを変え、関数 3 つと定数 2 つを足す -tests/containers/ -├── test_entrypoint_ai_settings.py # 変えない(35 件) -├── test_entrypoint_ai_settings_concurrent.py # 新設 -└── test_base_image_link_stage.py # 新設 -CHANGELOG.md # [Unreleased] の Fixed -docs/ -├── glossary/glossary.json # 5 語 -└── glossary.md # render で作り直す -``` - -足す関数と定数は、`DEVBASE_ENTRYPOINT_LIB_ONLY` の `return` より前に置く(要求の「前提とする取り決め」)。 -検査の手段は要求の「検証手段」のとおりで、`shellcheck containers/base/entrypoint.sh` と -`env -u DEVBASE_ROOT uv run --locked pytest tests/containers/ -q` を使う。 - -## 構造 - -型は持たない。変えるのは bash の関数の分け方と、処理の順序である。クラス図を描かないことは、要求の -対象範囲の「含まない」に書いた。 - -| 関数 | 入力 | 出力 | 失敗 | -| --- | --- | --- | --- | -| リンクの段の関数 | ホーム・共通のボリュームのルート・グループのボリュームのルート・グループ名・所有者(今と同じ) | 本体の起動ログ。ロックを待ち切れなかったときは警告の 1 行を標準エラーへ足す | 本体の終了コードをそのまま返す。共通のボリュームのルートを開けなければ非 0 | -| リンクの段の本体 | ホーム・2 つのルート・所有者 | 退避とリンクの起動ログ(今と同じ) | 退避かリンクが非 0 なら、そこで非 0 | -| リンクを張る関数 | リンクの位置・リンクの先・所有者(今と同じ) | 成功のときの起動ログ(今と同じ行の形) | 試行の上限まで正しくならなければ、標準エラーへ 1 行を出して 1(I3) | -| リンクの判定 | リンクの位置・リンクの先 | 無い | 正しければ 0、違えば 1。失敗の扱いは持たない | -| 退避の 1 エントリ | コピー元・コピー先・コピーしない直下の名前(今と同じ) | 作れてコピーしたときの起動ログ(今と同じ ` seeded: <コピー先>`)。既にあれば何も出さない(今と同じ) | 排他の作成が 2 か、コピーが失敗すれば非 0 | -| プレースホルダの作成 | エントリのパス(今と同じ) | 無い | 排他の作成が 2 なら非 0 | -| 排他の作成 | 種類(通常のファイル / ディレクトリ)・エントリのパス | 無い | 作れたら 0、既にあれば 1。作れず、読み直しても無ければ、標準エラーへ 1 行を出して 2(I9) | - -共有のリンクの位置が取る状態と遷移は次のとおりである。 - -```mermaid -stateDiagram-v2 - [*] --> 無い: 新しいグループのボリューム - 無い --> 正しいリンク: 張る - 別の先を指すリンク --> 無い: 消す - 実体 --> 無い: 消す - 正しいリンク --> 無い: 遅れて届いた「消す」 - 正しいリンク --> 実体: E7・引き金は分かっていない - 正しいリンク --> [*]: リンクの段が終わる -``` - -- 「別の先を指すリンク」は、指す先の無い壊れたリンクを含む -- 遅れて届いた「消す」は、正しくなる前に「違う」と判定を済ませていたプロセスが出す。修正前の手順の相手と、 - ロックなしで進んだ修正後のリンクの段から届く。ロックを取れたリンクの段どうしでは起きない(I5) -- 「無い」から「別の先を指すリンク」への遷移は無い。同じグループのコンテナは、同じ位置に同じ先を張る -- 「実体」から「正しいリンク」へ直に移る遷移は無い。置き換え(rename)を使わず、消してから張る(決定 4) - -## 入出力の契約 - -コマンド・環境変数・compose の生成物は変えない。2 つの上限は entrypoint の中の定数で、環境変数からは読まない -(テストは関数を読み込んだ後に定数を上書きして、短い待ち時間で確かめる)。変わるのは起動ログの行だけである。 - -| 場合 | 行 | 出力先 | 変更 | -| --- | --- | --- | --- | -| リンクが最初から正しい | ` ✓ <リンクの位置> (symlink exists)` | 標準出力 | 変えない | -| 位置に何かあり、消す | ` Removing existing <リンクの位置>...` | 標準出力 | 行の形は変えない。1 回目の試行でだけ出す | -| 張る | ` Creating symlink: <リンクの位置> -> <リンクの先>` | 標準出力 | 行の形は変えない。1 回目の試行でだけ出す | -| 張る操作か消す操作が失敗したが、読み直すと正しい | 何も足さない(`ln:` と `rm:` の行を出さない) | — | 新しい振る舞い(I4) | -| 試行の上限まで正しくならない | `ERROR: symlink を張れない: <リンクの位置> -> <リンクの先> (<最後に失敗したコマンドの出力>)` | 標準エラー | 新設(I3) | -| 排他の作成で作れなかったが、読み直すとエントリがある(ほかのプロセスが先に作った) | 何も足さない | — | 新しい振る舞い(I9) | -| エントリを作れず、読み直しても無い | `ERROR: エントリを作れない: <エントリのパス> (<失敗したコマンドの出力>)` | 標準エラー | 新設(I9)。今は `mkdir` か `cp` の行だけが出る | -| ロックを待ち切れなかった | `WARNING: リンクの段のロックを 10 秒待っても取れないため、ロックなしで進める: <共通のボリュームのルート>` | 標準エラー | 新設(I6) | - -終了コードは、リンクの段の関数もリンクを張る関数も、成功が 0、失敗が非 0 のままである。entrypoint は -`set -e` で動くため、リンクの段の関数が非 0 を返すとコンテナの起動が止まる(今と同じ)。 - -## 処理の流れ - -### リンクの段 - -```mermaid -sequenceDiagram - participant S as リンクの段の関数 - participant A as リンクの段の本体 - participant L as リンクを張る関数 - participant V as 2 つのボリューム - S->>V: ルートを用意する(E2) - alt flock がある - S->>V: 共通のボリュームのルートにロックを掛ける(最長 10 秒) - alt 取れた - V-->>S: ロック(E8) - else 待ち時間の上限を超えた - V-->>S: 取れない - S->>S: 警告の行を出し、ロックなしで進む(E8) - end - else flock が無い - S->>S: ロックなしで進む(E8) - end - S->>A: 呼ぶ - A->>V: 退避する(E3) - loop ホームのリンクと共有のリンク - A->>L: リンクの位置と先 - L->>V: プレースホルダを用意し、張り替えの試行を繰り返す - alt 正しくなった - L-->>A: 0(E4 か E5) - else 試行の上限を超えた - L-->>A: ERROR の行・1(本体はここで止まる) - end - end - A-->>S: 本体の終了コード - S->>V: ロックを手放す(E6) -``` - -- ルートの用意はロックの前に行う。ロックを掛ける相手のディレクトリが要るためである。中身は「無ければ作る・ - 書けなければ所有者を直す」で、重なっても結果は変わらない -- ロックは、リンクの段の関数が開いたサブシェルの中だけで持つ(決定 5)。本体が失敗してサブシェルが終わっても、 - 開いたファイルが閉じてロックは外れる。ロックを持ったプロセスが kill されたときも、カーネルが外す -- 図に含めない構成要素は、リンクの判定・退避の 1 エントリ・プレースホルダの作成・排他の作成・上限の定数・ - テスト 2 つ・変更履歴・用語集の 9 つである。リンクの判定は次の図に、退避の 1 エントリ・プレースホルダの作成・ - 排他の作成は「退避とプレースホルダ」の図にあり、残りは「構成要素の関係」の図にある - -### 張り替えの試行 - -```mermaid -graph TD - P[リンクの先の
プレースホルダを用意する] --> Q{最初から
正しいか} - Q -->|はい| OK1([✓ の行を出して 0]) - Q -->|いいえ| R[位置に何かあれば消す] - R --> T[張る: ln -sn] - T --> W{読み直すと
正しいか} - W -->|はい| OK2([所有者を揃えて 0]) - W -->|いいえ| N{20 回
試したか} - N -->|いいえ| R - N -->|はい| NG([ERROR の行を出して 1]) -``` - -- 消す操作と張る操作は、失敗してもそこで終わらない。標準エラーは捨てずに控え、最後の ERROR の行にだけ使う -- 張る操作には `-n` を付ける(決定 6)。位置が既にディレクトリを指すリンクなら、中へ作らずに失敗する(I2) -- 試行の間に待ち時間は置かない。次の試行が要るのは、相手のプロセスが位置を書き換えたときだけである - -修正前の手順で張る相手と重なったときの進み方は、次の 3 通りである。 - -| 重なり方 | 修正後の側 | 相手 | -| --- | --- | --- | -| 相手が先に張り、その後で修正後の側が張る | 張る操作は失敗するが、読み直すと正しいので 0 | 0 | -| 修正後の側が張った後で、相手が消して張る | 読み直した時点で正しければ 0。消された直後なら次の試行で張る | 0。修正後の側が先に張り直すと、相手は下の行の形になる | -| 修正後の側が張った後で、消し終えていた相手が張る | 0 | ファイルのリンクでは非 0 で終わる。ディレクトリのリンクでは入れ子のリンクを作って 0 で終わる(どちらも要求の範囲外) | - -修正前の手順の相手が消すのは、1 プロセスにつき 1 回である。同時起動が 8 個なら、正しいリンクが消されるのは -多くて 7 回で、試行は 9 回までに収まる。試行の上限の 20 回は、その 2 倍の余裕として置く。ロックなしで進んだ -修正後のリンクの段どうしが消し合うのは、確かめてから消すまでの間に相手が張ったときだけで、実測では上限に -達して非 0 で終わった回は無かった。 - -### 退避とプレースホルダ - -```mermaid -graph TD - Q{エントリは
既にあるか} -->|はい| SK([何もせず 0]) - Q -->|いいえ| C[排他の作成] - C -->|作れた| W[中身を書く] - W --> OK([0]) - C -->|既にあった| SK - C -->|作れず、読み直しても無い| NG([ERROR の行を出して非 0]) -``` - -- 通常のファイルは、bash の noclobber(`set -C` を付けたサブシェルの `>`)で空のファイルとして作る。作れた - プロセスが、その空のファイルへ中身を書く(退避は `cp -a` で上書きし、JSON のプレースホルダは `{}` を書く) -- ディレクトリは、`perl` から `mkdir(2)` を呼んで作る。base イメージの `mkdir`(uutils coreutils 0.8.0)は、 - 同時に呼ぶと 2 個以上が 0 で終わるため使わない。`perl` が無い環境では `mkdir` で作り、止めない(決定 10) -- 作る操作の終了コードが非 0 のときは、I1 と同じく読み直して決める。エントリがあれば「既にあった」(1)、 - 無ければ「作れない」(2)である -- 退避でディレクトリの子を `cp -a` でコピーするのは、作れたプロセスだけである。既にある子のディレクトリを - コピー先にする `cp -a` が走らないため、子の中へ重ねてコピーしない -- 作れなかったプロセスは、作れたプロセスが中身を書き終えるのを待たずに、リンクへ進む。全員が終わった後の - エントリは、作れたプロセスを最初に 1 個だけ走らせた後と同じである - -## 非機能の実現方式 - -| 大項目 | 要求の条件 | 実現方式 | 確かめ方 | -| --- | --- | --- | --- | -| 可用性 | ほかのコンテナを待つ仕組みを使う場合、待たせている側のプロセスが途中で kill されても、後から始めたリンクの段は 30 秒以内に終了コード 0 で終わる。kill の後に残ったもの(一時的な名前のエントリなど)があっても、次のリンクの段は終了コード 0 で終わる | ロックは `flock` で取り、持ち主のプロセスが終わるとカーネルが外す。持ち主が止まったまま終わらないときも、ロックの待ち時間の上限(10 秒)でロックなしへ切り替える。ロックのためのファイルも一時的な名前も作らないため、kill の後に残るものは、張りかけの状態(位置が「無い」)と、書きかけの退避のエントリだけである。前者は次のリンクの段が張り、後者は「既にある」として扱って 0 で終わる(今と同じ) | ロックを持ったプロセスを kill した後と、止めたままにした後のそれぞれで、リンクの段の所要と終了コードを見る。位置が「無い」状態から走らせて 0 を見る | -| 性能・拡張性 | 8 個同時に走らせたとき、8 個すべてが 30 秒以内に終わる(手元と CI)。1 個だけで走らせたときの所要は、修正前の所要に 1 秒を足した値を超えない。8 個を超える同時起動は確かめない(前提 2) | ロックの中で行うのはリンクの段の本体だけで、資格情報・clone・Docker-in-Docker はロックの外で動く。1 個だけのときに足すのは、`flock` の起動 1 回と、張った後の読み直しと、エントリが無いときの排他の作成(ディレクトリ 1 つにつき `perl` の起動 1 回。実測で 20 回が 13 ミリ秒)である。試行の間に待ち時間を置かない | 8 個同時の所要を測る。1 個だけの所要を、修正前の `entrypoint.sh` と並べて測る | -| 移行性 | 修正前と修正後のイメージのコンテナが同じグループに並ぶ(前提 1)。修正後のリンクの段がボリュームに残すものは、修正前の手順のリンクの段を失敗させない(並走のときも、entrypoint を前の版へ戻したときも)。ボリュームのデータの移行は無い | 修正後のリンクの段がボリュームに残すのは、修正前の手順が残すものと同じ並び(プレースホルダとリンク)だけである(I8)。並走は張り替えの試行で受ける | 修正後のリンクの段の後のボリュームの並びを、修正前の手順の後の並びと比べる。その後に修正前の手順を走らせて 0 を見る | -| システム環境 | base イメージに今入っている道具だけで動く(前提 5)。テストは Linux の CI で走る(前提 6) | 使うコマンドは `flock`(util-linux 2.41.3)・`ln`(uutils coreutils 0.8.0)・`rm`(GNU coreutils 9.7)・`readlink`・`perl`(5.40.1。Essential のパッケージ perl-base)で、どれも今の base イメージにある。entrypoint が `perl` を使うのは、この設計が初めてである。`ln -sn`・bash の noclobber・`perl` の `mkdir` は macOS でも同じ意味を持つ。`flock` が無い環境(macOS)ではロックなしで進むため、同時実行のテストは macOS でも走る | 実物のイメージのテスト(受け入れ条件 13)と、CI と macOS での pytest | - -## 決定の記録 - -決定 1〜10 と、その根拠にした実測は [issue-357-design-decisions.md](issue-357-design-decisions.md) にある。 - -## テスト設計 - -受け入れ条件 1〜7 は「同時に走らせる」テストで縛る。**1 回の同時実行では、修正前の手順でも通ることがある** -([実測](issue-357-design-decisions.md#決定の根拠にした実測)の 1 行目。Linux では 10 回のうち 7 回が通った)。 -テストは 1 件の中で同時実行を繰り返し、1 回でも条件が破れたら落ちる形にする。繰り返す回数は実装で測って決め、 -受け入れ条件 8 を満たすことを条件にする。 - -| 受け入れ条件・不変条件 | どの振る舞いで縛るか | どう壊したら落ちるべきか | -| --- | --- | --- | -| 受け入れ条件 1 | `settings.json` だけ通常のファイルの状態で 2 個同時に走らせると、2 個とも 0 で終わり、`settings.json` が共通のボリュームを指す | 張る操作が失敗したらそのまま非 0 で終わる(修正前の手順へ戻す) | -| 受け入れ条件 2 | 同じ状態で 8 個同時に走らせると、8 個とも 0 で終わり、共通のボリュームの `settings.json` の中身が変わらない | 修正前の手順へ戻す。消す対象をリンクの先へ向ける | -| 受け入れ条件 3 | 2 つのボリュームが空で、どのホームにも同じ `settings.json` がある状態で 8 個同時に走らせた後の、2 つのボリュームの並び・リンクの先・ファイルの中身が、1 個だけ走らせた後と同じ | 修正前の手順へ戻す。張る操作の `-n` を外す | -| 受け入れ条件 4 | 共通のボリュームに中身があり、グループのボリュームが空の状態で 8 個同時に走らせると、8 個とも 0 で終わり、共有のリンク 5 本が共通のボリュームを指す | 修正前の手順へ戻す | -| 受け入れ条件 5 | `plugins` の位置が、中にファイルを持つ実体のディレクトリの状態で 8 個同時に走らせると、8 個とも 0 で終わり、`plugins` が共通のボリュームを指す | 実体を消さずに張る | -| 受け入れ条件 6 | 1 本が別の先を指す状態と、壊れたリンクである状態のそれぞれで 8 個同時に走らせると、8 個とも 0 で終わり、そのリンクが共通のボリュームを指す | 先を比べずに、symlink であるだけで正しいと判定する | -| 受け入れ条件 7 | 修正後のリンクの段 1 個と、同じ共有のリンクを修正前の手順で張る相手 1 個を同時に走らせると、修正後の側が 0 で終わり、共有のリンクが共通のボリュームを指す | 読み直しを外し、張る操作の終了コードで決める | -| 受け入れ条件 8 | 受け入れ条件 1 のテストを、修正前の `entrypoint.sh`(設計のブランチの起点の版)へ向けて 3 回続けて走らせると、3 回とも落ちる。完了判定で 1 度確かめる | 繰り返す回数を 1 回に減らすと、3 回のうちに通る回が出る | -| 受け入れ条件 9・I3 | グループのボリュームの `.claude` に書き込めない状態で走らせると、非 0 で終わり、張れなかったリンクの位置のパスを含む行が標準エラーに出る | 試行の上限の後で 0 を返す。ERROR の行からパスを外す | -| 受け入れ条件 10・I4 | 受け入れ条件 1〜7 のテストで 0 で終わったリンクの段の出力に、`ln:` で始まる行が無い | 張る操作の標準エラーをそのまま流す | -| 受け入れ条件 11・I8 | 受け入れ条件 1〜7 のテストの後、グループのボリュームの `.claude` の並びが共有のリンク 5 本とそのテストが置いたものだけで、2 つのボリュームのルートにロックのためのエントリが無い | ロックをファイルに掛ける。一時的な名前で張る | -| 受け入れ条件 12 | 既存の `test_entrypoint_ai_settings.py` を変えずに走らせ、35 件とも通る | 起動ログの行の形か引数の形を変える | -| 受け入れ条件 13 | base イメージのコンテナ 8 個を、受け入れ条件 1 の状態にした使い捨てのボリュームで同時に起動すると、8 個とも entrypoint が 0 で終わる。使ったボリュームとコンテナは消す(決定 9) | 修正前の `entrypoint.sh` をマウントすると、繰り返すうちに非 0 が出る | -| I1 | 消した直後に、ほかのコンテナが同じリンクを張り終えた状態を作ると、張る操作は失敗するが、リンクを張る関数は 0 で返る | 読み直しを外す | -| I2 | 同じ状態を `plugins` で作ると、共通のボリュームの `plugins` の中に `plugins` という名前のリンクができない | 張る操作の `-n` を外す | -| I5 | 別のプロセスがリンクの段のロックを持っている間、リンクの段は退避もリンクも始めずに待ち、手放された後に進んで 0 で終わる | ロックを取らずに本体を呼ぶ | -| I6 | ロックを持ったプロセスが手放さないままでも、待ち時間の上限の後に警告の行を出して 0 で終わる。`flock` が見つからない状態で 8 個同時に走らせても、8 個とも 0 で終わる | 待ち時間の上限を外す。`flock` が無いときに非 0 で終わる | -| I9(ロックなしの退避) | `flock` が見つからない状態で、2 つのボリュームが空、どのホームにも同じ `~/.claude`(`plugins` の下に子のディレクトリを 12 個・`settings.json`)がある状態から 8 個同時に走らせると、8 個とも 0 で終わり、2 つのボリュームの並びとファイルの中身が、1 個だけ走らせた後と同じ | 退避を今の手順(無ければディレクトリを作って子をコピーする)へ戻す | -| I9(作れるのは 1 つ) | 無いエントリに対して排他の作成を 8 個同時に呼ぶと、通常のファイルでもディレクトリでも、0 を返すのは 1 個だけである。既にあるエントリに対して呼ぶと 1 を返し、中身を変えない | あるかを確かめてから作る 2 段の手順にする | -| I9(base イメージの道具) | base イメージの使い捨てのコンテナの中の一時ディレクトリで、上の 2 行と同じことを確かめる(実物のイメージのテスト。ボリュームは使わない) | ディレクトリを `mkdir` で作る(uutils の `mkdir` は 2 個以上が 0 で終わる。CI の runner の `mkdir` では落ちない) | -| I9(作れない) | コピー先の親のディレクトリに書き込めない状態で退避を走らせると、非 0 で終わり、エントリのパスを含む行が標準エラーに出る(root では状態を作れないため、受け入れ条件 9 と同じく skip する) | 排他の作成が 2 を返したときに 0 で返る | -| 決定 10(`perl` が無い) | `perl` が見つからない状態で 1 個だけ走らせると 0 で終わり、2 つのボリュームの並びとファイルの中身が、`perl` がある状態と同じ | `perl` が無いときに非 0 で終わる | -| I7 | リンクの段の関数から戻った後、呼んだシェルが生きている間に、別のプロセスが待たずにロックを取れる | サブシェルを使わず、呼び出したシェルで fd を開いたままにする | -| 可用性 | ロックを持ったプロセスを途中で kill すると、後から始めたリンクの段が 30 秒以内に 0 で終わる。共有のリンクの位置が「無い」状態(張りかけで終わった後)から走らせても 0 で終わる | ロックを、プロセスが終わっても残る形(ファイルの有無)で持つ | -| 性能・拡張性 | 受け入れ条件 2〜6 のテストで、8 個すべてが 30 秒以内に終わる。1 個だけの所要が、修正前の `entrypoint.sh` の所要に 1 秒を足した値を超えない | 試行の間に長い待ち時間を置く。ロックを待ち時間の上限まで必ず待つ | -| 移行性 | 修正後のリンクの段を走らせた後に、修正前の手順のリンクの段を同じボリュームで走らせると 0 で終わる | ボリュームに、修正前の手順が実体として消しに行くエントリを残す | - -修正前の手順の相手(受け入れ条件 7・移行性)は、`entrypoint.sh` の今のリンクを張る関数をテストの側へ -コピーしたものを使う。実装の後は `entrypoint.sh` から読めなくなるためである。 - -## 未確認のまま残ること - -| 項目 | 内容 | -| --- | --- | -| 同時起動の上限を 8 個と置くこと(要求の前提 2) | 利用者が設計の承認のときに決める。試行の上限の 20 回は、8 個を前提にした値である | -| 動いているコンテナの中のツールとの競合(要求の前提 4) | 入れない前提で設計した。利用者が #372 の扱いと合わせて決める | -| pytest を並列(`-n auto`)で流したときの、同時実行のテストの所要と、修正前の手順に対する落ち方 | 実測は直列で行った。繰り返す回数は、並列で流した状態で実装のときに測って決める | -| CI の runner での `ln` | GitHub Actions の runner の `ln` は GNU coreutils で、uutils の `ln` での確認は実物のイメージのテスト(開発者の端末)だけになる。base イメージの `ln` が入れ替わったときは、受け入れ条件 13 のテストで気づく | -| 退避が 10 秒を超える大きさのとき | 初回の起動で、派生イメージが `~/.claude` に大きなディレクトリを持つと、退避の間に待つ側がロックの待ち時間の上限を超え、ロックなしで進む。共有のリンクは正しくなり、退避は排他の作成で作れた 1 つのプロセスだけが書く(I9)。待つ側は、退避が終わる前にリンクを張って先へ進むため、起動の直後に共通のボリュームの中身が揃っていない時間がある(修正前と同じ)。そのようなイメージがあるかは調べていない | -| ロックを取れず、ホームの中身が違うコンテナが同時に退避するとき | エントリごとに、排他の作成で作れた 1 つのコンテナの中身が丸ごと残る。エントリが違えば、作れたコンテナも違うことがある(`plugins` はコンテナ A、`settings.json` はコンテナ B)。1 個ずつ順に走らせると、最初の 1 個が持つエントリはすべてその中身になるため、結果が違う。受け入れ条件 3 の前提(どのホームにも同じ中身)の外で、ロックを取れたときは起きない | -| 排他の作成で作れたプロセスが、中身を書き終える前に kill されたとき | 書きかけのエントリ(空の `settings.json`・途中までの `plugins`)が残り、次のリンクの段は「既にある」として退避しない。1 個だけで走らせて退避の途中で kill されたときと同じである。通常のファイルでは、空のファイルを作ってから `cp` が中身を書くまでの時間が、今より長くなる | -| base イメージの `mkdir` が排他でないこと | uutils coreutils 0.8.0 の `mkdir` だけで確かめた。版が上がって振る舞いが変わっても、`perl` の `mkdir` を使い続ける。ほかに `mkdir` の排他に頼る箇所が entrypoint にあるかは調べていない | -| 修正前の手順が過去に作った入れ子のリンク | 片付けは行わない。動いているコンテナ 1 つで、共通のボリュームの `plugins/plugins`・`skills/skills`・`commands/commands` が無いことを確かめた(2026-10-02)。ほかの利用者のボリュームは確かめていない | -| 修正前のイメージのコンテナ | 建て直すまで、同時起動で落ちることと、入れ子のリンクを作ることが残る(要求の対象範囲の「含まない」) | diff --git a/issues/issue-357-requirements.md b/issues/issue-357-requirements.md deleted file mode 100644 index ecacb961..00000000 --- a/issues/issue-357-requirements.md +++ /dev/null @@ -1,178 +0,0 @@ -# #357: 同じアカウントグループのコンテナを同時に起動すると entrypoint の symlink 作成が競合して 1 つが落ちる - -正は課題の本文(#357)で、この文書はその写しである。`spec-copy.py write` で作り直す。手で直さない。 - -## 依頼(原文) - -起票時の本文をそのまま写す。グループの実名だけは `<グループ>` に置き換えた(公開のリポジトリに固有の名前を置かない決まりのため)。 - -> ## 何を見つけたか -> -> 同じアカウントグループのコンテナを同時に起動すると、グループボリュームの symlink を張る処理が競合し、1 つが `ln: Already exists` で終了する(終了コード 1)。 -> -> - 再現(2026-09-30): ホストの再起動の後、グループ `<グループ>` の 4 コンテナ(プロジェクトが別々で `devbase_home_<グループ>` を共有)を `docker start` で同時に起動した。3 つは起き、1 つが次のログで落ちた -> -> ```text -> Creating symlink: /persistent/group/.claude/settings.json -> /persistent/ai/.claude/settings.json -> ln: Already exists -> ``` -> -> - 落ちたコンテナをもう一度 `docker start` すると `✓ (symlink exists)` の経路で起きる(他のコンテナが同じ symlink を張り終えているため) -> -> ## 原因(推定) -> -> `containers/base/entrypoint.sh` の `devbase_link_setting()` は「symlink が正しいか見る → 違えば消す → `ln -s`」を排他なしで行う。2 つのプロセスが同時に「違う」と判定すると、後から `ln -s` した方が既存のリンクに当たって失敗し、`set -e` で entrypoint ごと終わる。 -> -> ## 直すこと -> -> - `ln -s` を冪等にする(例: 一時名で作って `mv -T` で置き換える、または `ln -sfn` にして失敗の後に読み直す)か、グループボリュームの上で `flock` を取る -> - 同時に 2 つ走らせても両方が成功するテストを足す -> -> ## 対象外 -> -> - ホストの再起動の後にコンテナを自動で起こす仕組み - -## 目的 - -- ボリュームを共有するコンテナを同時に起動しても、すべてのコンテナが起動し、共有のリンクが正しい状態で残る -- 落ちたコンテナを利用者が見つけて起こし直す手間(今の回避策)を無くす - -## 現状(2026-10-02 に調べたこと) - -原文の推定を、コードと実環境の読み取りで確かめた。 - -| 調べたこと | 結果 | -| --- | --- | -| 競合する場所 | `devbase_link_setting()` が張るリンクのうち、複数のコンテナが同じ 1 本を使うのは共有のリンク 5 本(`/persistent/group/.claude/` の `plugins` / `skills` / `commands` / `CLAUDE.md` / `settings.json`)だけである。ホーム直下のリンクはコンテナごとに別で、競合しない | -| 競合が起きる条件 | リンクの段が共有のリンクを張り直すとき。実環境では `settings.json` が運用中に通常のファイルへ置き換わっており(#372)、9 コンテナの起動 16 回のうち 11 回で張り直していた。新しいグループのボリュームの初回は 5 本とも張る | -| 落ちたときのログ | 落ちた 2 件とも、`Removing existing ...` の行が無いまま `Creating symlink: ...` → `ln: Already exists` で終わっていた(相手が実体を消した後、相手より遅く `ln -s` した) | -| 再現のしやすさ | `devbase_setup_ai_settings` を、別々のホーム・同じ 2 つのボリューム(`settings.json` が通常のファイル)へ向けて 4 個同時に走らせると、20 回のうち 20 回で 1 個以上が非 0 で終わった(macOS・bash 5.3・Docker なし) | -| base イメージの道具 | 動いているコンテナ(Ubuntu 26.04)では `ln` が uutils coreutils 0.8.0、`mv` が GNU coreutils 9.7 で、`flock` が入っている。失敗の文言 `Already exists` は uutils の `ln` のもの(GNU は `File exists`) | -| 同じ競合のほかの入口 | scale 2 のプロジェクトを新しく足した `devbase up` でも 2 台目が同じ形で落ちた(#371 の再現)。ホストの再起動に限らない | - -## 前提 - -- 前提 1: 修正は base イメージの建て直しでコンテナへ届く。派生イメージとプロジェクトのイメージは別々に建て直すため、同じグループに修正前と修正後のイメージのコンテナが並ぶ期間がある -- 前提 2: 同時起動の数は 8 個までを確かめる(観測した最大は 1 グループ 4 個)。8 個を超える数は確かめない -- 前提 3: リンクの位置に実体(通常のファイル・ディレクトリ・別の先を指すリンク)があるときは、消して張り直す。この決まりは変えない(是非は #372 で決める) -- 前提 4: 競合の相手として扱うのは、ほかのコンテナのリンクの段だけである。動いているコンテナの中のツールが同じ位置へ書くこととの競合は、受け入れ条件に入れない -- 前提 5: base イメージに道具を足さず、道具の版も変えない。今のイメージに入っている道具だけで実現する -- 前提 6: テストは今の `tests/containers/test_entrypoint_ai_settings.py` と同じく、Docker を使わずに entrypoint の関数を一時ディレクトリへ向けて走らせる。CI(Linux)では必ず走る。開発者の macOS で走れないテストは、理由を示して skip する - -## 対象範囲 - -含む: -- 共有のリンクを張る処理が、同時起動で失敗しないこと -- リンクの段の全体(ボリュームのルートの用意・イメージの設定の退避・プレースホルダの作成・リンク)を同時に走らせた結果が、1 つずつ順に走らせた結果と同じになること -- 同時に走らせて確かめるテストを足すこと -- 修正前のイメージのコンテナと並んで起動したときに、修正後のコンテナが落ちないこと - -含まない: -- ホストの再起動の後にコンテナを自動で起こす仕組み(原文の対象外) -- リンクの位置に実体があるときに消して張り直す決まりの是非と、`settings.json` の置き場(#372) -- `devbase up` / `devbase scale` で 1 台が落ちたときの後処理(#371・#224)と、落ちたコンテナを起こし直す仕組み -- 修正前のイメージのコンテナが落ちること(建て直すまで今の振る舞いのまま残る) -- リンクの段の外の entrypoint の処理(Git・AWS・GCP の資格情報、リポジトリの clone、Docker-in-Docker)の同時実行 -- 動いているコンテナの中のツールとの競合(前提 4) -- 設計の成果物のうちクラス図(変えるのは bash の関数で、型を持たないため、描く対象が無い) - -## ドメインイベント - -| # | イベント | 引き金 | 失敗したとき | 順序の前提 | -| --- | --- | --- | --- | --- | -| E1 | リンクの段が始まった | コンテナの起動(`devbase up` / `devbase scale` / `docker start`) | — | — | -| E2 | 2 つのボリュームのルートを用意した | E1 | 書き込めなければ非 0 で終わる(今のまま) | E1 | -| E3 | イメージの設定を共通のボリュームへ退避し、プレースホルダを作った | E2。実体がまだ無いエントリだけ | 書き込めなければ非 0 で終わる(今のまま) | E2。**ほかのコンテナの E3 と重ならないと仮定している** → 受け入れ条件 3 | -| E4 | 共有のリンクが正しいと確かめた | E3。リンクが既に正しい先を指している | — | E3 | -| E5 | 共有のリンクを張り直した | E3。リンクが無い・別の先を指す・実体がある | 同時起動の相手が先に張っていた → 成功として扱う(受け入れ条件 1〜5)。張れない状態 → 非 0 で終わり、対象のパスを出す(受け入れ条件 9) | E3。**確かめてから張るまでに、ほかのコンテナが同じ位置を変えないと仮定している** → 受け入れ条件 1〜6 | -| E6 | リンクの段が終わった | E4 か E5 が 5 本ぶん済んだ | — | E4 / E5 | -| E7 | 共有のリンクが通常のファイルへ置き換わった | **引き金が分かっていない** → 前提 3・対象範囲の「含まない」・#372 | — | E6 の後、コンテナが動いている間 | - -## 用語 - -| 用語 | 意味 | -| --- | --- | -| アカウントグループ | 用語集のとおり(`DEVBASE_ACCOUNT_GROUP` の値) | -| グループのボリューム | 用語集のとおり(コンテナの `/persistent/group`) | -| 共通のボリューム | すべてのコンテナが `/persistent/ai` にマウントする 1 つのボリューム。グループをまたいで共有する | -| 共有のリンク | グループのボリュームの `.claude` の下にあり、共通のボリュームの同じ名前のエントリを指す symlink。`plugins` / `skills` / `commands` / `CLAUDE.md` / `settings.json` の 5 本。同じグループのすべてのコンテナが同じ 1 本を使う | -| リンクの段 | entrypoint が AI 設定のリンクを張る処理(`devbase_setup_ai_settings` の 1 回の実行) | -| 同時起動 | 2 つ以上のコンテナのリンクの段が、時間の上で重なって走ること | -| 修正前の手順 | この変更より前のリンクの張り方。「正しいかを見る → 違えば消す → `ln -s`」を、ほかのコンテナを待たずに行う | - -## 受け入れ条件 - -「同時に走らせる」は、別々のホームと、同じ共通のボリューム・同じグループのボリュームを渡したリンクの段を、同時に始めることを指す。 - -- [ ] 1. 前提: 共有のリンクのうち `settings.json` だけが通常のファイルで、残り 4 本は正しい - 操作: リンクの段を 2 個同時に走らせる - 結果: 2 個とも終了コード 0 で終わり、`settings.json` は共通のボリュームの `settings.json` を指す symlink になる -- [ ] 2. 受け入れ条件 1 と同じ前提で 8 個同時に走らせると、8 個とも終了コード 0 で終わり、共通のボリュームの `settings.json` の中身は走らせる前と同じである -- [ ] 3. 前提: 共通のボリュームもグループのボリュームも空で、どのホームにも同じ中身の `~/.claude/settings.json` がある - 操作: リンクの段を 8 個同時に走らせる - 結果: 8 個とも終了コード 0 で終わる。2 つのボリュームのファイルとディレクトリの並び・リンクの先・ファイルの中身が、同じ前提で 1 個だけ走らせたときと同じになる -- [ ] 4. 前提: 共通のボリュームに中身があり、グループのボリュームが空である - 操作: リンクの段を 8 個同時に走らせる - 結果: 8 個とも終了コード 0 で終わり、共有のリンク 5 本がそれぞれ共通のボリュームの同じ名前のエントリを指す -- [ ] 5. 共有のリンク `plugins` の位置に、中にファイルを持つ実体のディレクトリがある状態で 8 個同時に走らせると、8 個とも終了コード 0 で終わり、`plugins` は共通のボリュームを指す symlink になる -- [ ] 6. 共有のリンクの 1 本が別の先を指している状態、指す先の無い壊れたリンクである状態のそれぞれで 8 個同時に走らせると、8 個とも終了コード 0 で終わり、そのリンクは共通のボリュームを指す -- [ ] 7. 修正後のリンクの段 1 個と、同じ共有のリンクを修正前の手順で張る相手 1 個を同時に走らせると、修正後の側は終了コード 0 で終わり、共有のリンクは共通のボリュームを指す(相手が非 0 で終わることは許す) -- [ ] 8. 受け入れ条件 1〜6 の同時に走らせるテストは、修正前の `entrypoint.sh` に当てると失敗する(受け入れ条件 1 か 2 のテストを 3 回続けて走らせ、3 回とも失敗する) -- [ ] 9. グループのボリュームの `.claude` に書き込めない状態でリンクの段を走らせると、非 0 で終わり、張れなかったリンクのパスを含む行が出る(同時起動の競合ではない失敗を、成功として扱わない) -- [ ] 10. 受け入れ条件 1〜7 で終了コード 0 で終わったリンクの段の出力に、`ln:` で始まる行が無い -- [ ] 11. 受け入れ条件 1〜7 のあと、グループのボリュームの `.claude` の下に、張り替えの途中で使う一時的な名前のエントリが残っていない -- [ ] 12. 既存の `tests/containers/test_entrypoint_ai_settings.py` の 35 件が、テストを書き換えずに通る -- [ ] 13. 前提: base イメージ(`ln` が uutils coreutils のもの)と修正後の `entrypoint.sh`、使い捨ての共通のボリュームとグループのボリューム(受け入れ条件 1 の状態にしたもの) - 操作: 8 個のコンテナを同時に起動する - 結果: 8 個とも entrypoint が終了コード 0 で終わる。利用者のボリュームには触れない - -## 非機能の条件 - -| 大項目 | 条件 | -| --- | --- | -| 可用性 | ほかのコンテナを待つ仕組みを使う場合、待たせている側のプロセスが途中で kill されても、後から始めたリンクの段は 30 秒以内に終了コード 0 で終わる。kill の後に残ったもの(一時的な名前のエントリなど)があっても、次のリンクの段は終了コード 0 で終わる | -| 性能・拡張性 | 8 個同時に走らせたとき、8 個すべてが 30 秒以内に終わる(手元と CI)。1 個だけで走らせたときの所要は、修正前の所要に 1 秒を足した値を超えない。8 個を超える同時起動は確かめない(前提 2) | -| 移行性 | 修正前と修正後のイメージのコンテナが同じグループに並ぶ(前提 1)。修正後のリンクの段がボリュームに残すものは、修正前の手順のリンクの段を失敗させない(並走のときも、entrypoint を前の版へ戻したときも)。ボリュームのデータの移行は無い | -| システム環境 | base イメージに今入っている道具だけで動く(前提 5)。テストは Linux の CI で走る(前提 6) | - -## 影響 - -| 対象 | 影響 | -| --- | --- | -| 公開インタフェース | 変わらない(コマンド・環境変数・compose の生成物)。起動ログの `✓ ... (symlink exists)` と `Creating symlink: ...` の行の形は保つ | -| データ | スキーマの変更も移行も無い。共有のリンクの指す先は変わらない | -| 既存の振る舞い | 同時起動で落ちなくなる。効くのは base イメージを建て直したコンテナからである。利用者に見える変更のため `CHANGELOG.md` の `[Unreleased]` の `Fixed` に書く | - -## 検証手段 - -| 項目 | 手段 | -| --- | --- | -| テスト | `env -u DEVBASE_ROOT uv run --locked pytest tests/containers/ -q`(全体は `.ndf/project.json` の `test.suites` のコマンド) | -| 静的解析・型検査 | `shellcheck containers/base/entrypoint.sh`(CI の ShellCheck の検査ジョブと同じ版 v0.11.0)、`uv run ruff check tests/` | -| 起動 | 受け入れ条件 13。`docker run` で、使い捨ての名前のボリューム 2 つとコンテナ 8 個を作り、終了コードを集めてから消す。`devbase up` は使わない | -| 手動確認 | 無し(人にしか見られない条件は無い) | - -## 前提とする取り決め - -| 項目 | 参照先 / 決めたこと | -| --- | --- | -| プロジェクト構造 | entrypoint の関数は `containers/base/entrypoint.sh` の中で、`DEVBASE_ENTRYPOINT_LIB_ONLY` の `return` より前に置く(テストが関数だけを読み込むため)。テストは `tests/containers/` に置く。`lib/devbase` の起動の側(`up` / `scale`)は触らない | -| コーディング規約 | `CONTRIBUTING.md`。シェルは CI の ShellCheck の検査ジョブ、Python は Ruff で確かめる | -| テスト戦略 | 関数の水準(Docker なし)で、同時起動と状態の境界(受け入れ条件 1〜12)を固定する。実物のコンテナでは、base イメージの道具で動くこと(受け入れ条件 13)を完了判定で 1 回確かめる | - -## 境界 - -| 区分 | 内容 | -| --- | --- | -| 常に行う | 既存のテストと ShellCheck の実行、`CHANGELOG.md` への記入、確かめに使ったボリュームとコンテナの片付け | -| 確認してから行う | base イメージに入れる道具や版を変えること、利用者の実際の環境で `devbase up` を打つこと、利用者のグループのボリュームへ書き込む確認 | -| 行わない | リンクの位置の実体を消す決まりの変更(#372)、`devbase up` / `devbase scale` の側での起動のやり直しや直列化、依頼の範囲の外の整形 | - -## 未決 - -| 項目 | 誰が決めるか | 期限 | -| --- | --- | --- | -| 張り替えを冪等にするか、ほかのコンテナを待つ仕組みにするか、両方か。受け入れ条件 7(修正前の手順の相手と並んでも落ちない)は、待つ仕組みだけでは満たせない見込みである | 設計 | 設計の承認まで | -| 同時起動の上限を 8 個と置くこと(前提 2)でよいか | 利用者 | 設計の承認まで | -| 動いているコンテナの中のツールとの競合(前提 4)を、この課題へ入れるか | 利用者 | #372 の扱いが決まるまで。入れない前提で進める | -| macOS で走れないテストが出た場合に、skip でよいか(前提 6) | 設計 | 設計の承認まで |