From 84977c356b400359671f95d68e44e508699f016b Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Tue, 13 Oct 2026 00:55:02 +0900 Subject: [PATCH 1/3] =?UTF-8?q?Docs:=20snapshot-series.md=20=E3=82=92?= =?UTF-8?q?=E7=A2=BA=E5=AE=9A=E4=BB=95=E6=A7=98=E3=81=AB=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/specifications/snapshot-series.md | 124 ++++++-- issues/issue-458-design.md | 382 ------------------------- issues/issue-458-requirements.md | 194 ------------- 3 files changed, 100 insertions(+), 600 deletions(-) delete mode 100644 issues/issue-458-design.md delete mode 100644 issues/issue-458-requirements.md diff --git a/docs/specifications/snapshot-series.md b/docs/specifications/snapshot-series.md index 90fe72e3..104705f1 100644 --- a/docs/specifications/snapshot-series.md +++ b/docs/specifications/snapshot-series.md @@ -32,6 +32,7 @@ - `devbase up` / `devbase down` / `devbase snapshot rotate` / TUI のローテーションの規則 - 世代の場所の検証(`_safe_snap_dir`)。`create` / `restore` / `copy` / `delete` / `rotate` が共有する。 `list` は `rotate` と同じ判定で不正なエントリを一覧から外す +- 世代の作り直し(既にある世代のディレクトリへ full を作るときの手順・失敗の扱い・作り直しの途中の世代の復元の拒否) - ログの文言 含まない: @@ -40,7 +41,7 @@ 系列に入り、同じ規則で消える。`pre-restore-*` は復元する世代の対象ボリュームの組で作られるため、 復元した世代と同じ系列に入り、その系列の最新の世代になる(次の自動スナップショットはそこへ差分を積む)。 名前付きの世代・`copy` の世代を守る仕組みは持たない。長く残す世代は `backups/` の外へ複製する -- 復元の対象・順序・失敗時の案内(変えていない) +- 復元の対象・順序・失敗時の案内(作り直しの途中の印を持つ世代を復元の前に止めることを除く) - `devbase snapshot list` / `devbase status` の表示(場所が不正なエントリを外すことを除く)。`status` の「最新」は `snapshot.yml` の最後の エントリ(最も新しく**作られた**世代)で、直前に差分を積んだ世代とは一致しないことがある - 保持をバイト数で制限すること @@ -55,8 +56,8 @@ | 系列 | 対象ボリュームの組が同じ世代の集まり。`volumes` が無い・空・dict でないエントリ(PLAN39 より前の旧レイアウト)は `{'': devbase_home_ubuntu}` の組として 1 つの系列になる | | 系列の最新の世代 | 系列の中で `created_at` が最も新しい世代。同じ値なら `snapshot.yml` で後ろのもの | | 全体の上限 | 系列をまたいで数えた世代の数の上限(`max_total`) | -| 復元前バックアップ | `restore` が書き戻す前に自動で作るフルの世代 `pre-restore-<時刻>`。復元する世代の対象ボリュームの組を控える。実行時の `DEVBASE_ACCOUNT_GROUP` には依らない。控えに失敗したら警告を出してそのディレクトリを消し、控え無しで復元を続ける | -| 世代の作り直し | 既にある世代のディレクトリへ、前の `snapshot.snar` を引き継がずに新しい full を作り、前の差分と状態のファイルを除くこと。`--full` を既存の名前へ打ったとき・snar の無い世代への差分の指定・同じ秒に重なった復元前バックアップで起きる | +| 復元前バックアップ | `restore` が書き戻す前に自動で作るフルの世代 `pre-restore-<時刻>`。復元する世代の対象ボリュームの組を控える。実行時の `DEVBASE_ACCOUNT_GROUP` には依らない。控えに失敗したら警告を出し、その呼び出しで作ったディレクトリだけを消して(既にあった同名の世代は消さない)、控え無しで復元を続ける | +| 世代の作り直し | [用語集](../glossary.md)の語。起きる場面と手順は下の「[世代の作り直し](#世代の作り直し)」 | ## 構成要素 @@ -66,7 +67,7 @@ | 積み先の判定 | 同 `auto_snapshot_target` | 積み先の世代の名前か、新しい世代を作るべきこと(`None`)を返し、理由をログへ出す | | 最小間隔の元 | 同 `last_snapshot_time(volumes=None)` | 渡された組の系列の世代のアーカイブの mtime の最大を返す | | 保持 | 同 `rotate(keep, max_total)` と、副作用の無い `_rotation_plan` | 削除候補を決め、場所を検証してから消す | -| 世代の作り直し | 同 `_create_full` / `_install_rebuilt_full` / `_discard_rebuild_temps` | 前のファイルを持つ世代へ full を作るとき、仮の名前へ控えてから決まった順で入れ替える(下の「世代の作り直し」) | +| 世代の作り直し | 同 `_create_full` / `_has_previous_files` / `checked_backup_command` / `_install_rebuilt_full` / `_replace_snap_meta` / `_discard_rebuild_temps`、`restore`(印の検査)。仮の名前は定数 `FULL_ARCHIVE_TMP`・`SNAR_FILE_TMP`・`META_FILE_TMP`、印は `REBUILDING_KEY` | 前のファイルを持つ世代へ full を作るとき、仮の名前へ控えてから決まった順で入れ替える。作り直しの途中の印を持つ世代の復元を止める(下の「世代の作り直し」) | | 世代の場所の検証 | 同 `_safe_snap_dir` | 名前・シンボリックリンク・`backups/` の外を `SnapshotError` で止める | | 自動スナップショット | `lib/devbase/commands/container.py` の `_auto_snapshot` / `_should_skip_by_interval` | 最小間隔を系列で判定し、判定の結果に従って `create` と `rotate()` を呼ぶ | | 自動ローテーション | 同 `cmd_down` | `mgr.rotate()` を既定の引数で呼ぶ | @@ -254,9 +255,23 @@ classDiagram ### 世代の作り直し `_create_full` は、世代に前のファイル(`full.tar.zst`・`snapshot.snar`・`incr-*.tar.zst` のどれか)があれば -世代の作り直しとして控える。無ければ本来の名前(`full.tar.zst`・`snapshot.snar`)へ直接控え、最後に +世代の作り直しとして控える(`_has_previous_files`)。無ければ本来の名前(`full.tar.zst`・`snapshot.snar`)へ直接控え、最後に `meta.yml` を書く(新しい世代の控えのコマンドと `meta.yml` はこの規則の前と同じ)。 +作り直しになる場面は、`_create_full` を呼ぶ 3 つの経路のうち、世代のディレクトリに前のファイルがあるものである。 + +| 場面 | 呼び出し元 | +| --- | --- | +| 既にある世代の名前へ `devbase snapshot create --name <名前> --full` を打つ | `create` | +| `snapshot.snar` の無い世代へ差分を指定する(`create --name <名前>`・自動スナップショット)。組・控えの起点・除外の一覧の検査を通った後で full へ切り替わる | `_create_incremental` | +| 復元前バックアップの名前 `pre-restore-<時刻>`(秒まで)が既にある世代と重なる | `_backup_before_restore` | + +既にある名前への `--full` は拒まずに受け付ける。利用者の文書が名前付きの `--full` を例に挙げ、同じ名前で取り直す +使い方を禁じていないためである。前の時点を残したい利用者は、別の名前で作るか、先に `snapshot copy` で写す。 + +**判定を前のファイルの有無に置く理由**: 新しい世代の控えのコマンド・アーカイブ・`meta.yml` を 1 文字も変えずに済む。 +`create` の `is_new` で分けると、snar の無い世代への差分の切り替えと、重なった復元前バックアップを拾えない。 + 作り直しの手順: 1. 前の中断の残りの仮の名前のファイル(`full.tar.zst.new`・`snapshot.snar.new`・`meta.yml.new`)を消す @@ -274,17 +289,42 @@ classDiagram 4. `full.tar.zst.new` を `full.tar.zst` へ改名する 5. `meta.yml` を新しい控えの内容(印無し)へ置き換える 6. `snapshot.snar.new` を `snapshot.snar` へ改名する -4. INFO を 1 行出す。`snapshot.yml` のエントリは `create` / `_backup_before_restore` が今の規則で更新する - (`incremental_count` と `updated_at`。`created_at` は変えない) +4. INFO を 1 行出す(名前と除いた差分の本数)。`snapshot.yml` のエントリは `create` / `_backup_before_restore` が + 今の規則(`_update_global_metadata`)で更新する。`incremental_count` は 0、`updated_at` は今の時刻、`volumes` は + 作り直した `meta.yml` の組になり、`created_at` は変えない。エントリは 1 件のままである -`meta.yml` の置き換えは `meta.yml.new` へ書いてから改名し、途中まで書いた `meta.yml` を残さない。 +`meta.yml` の置き換えは `meta.yml.new` へ書いてから改名し(`_replace_snap_meta`)、途中まで書いた `meta.yml` を残さない。 作り直した `meta.yml` は `type: full`・`files: [full.tar.zst]`・`incremental_count: 0`・作り直した時点の -`volumes`・`exclude`・`created_at` を持つ。 +`volumes`・`exclude`・`created_at` を持つ(新しい世代と同じ `_full_meta` で組む)。`meta.yml` の `created_at` を読む処理は無く、 +系列の新旧とローテーションは `snapshot.yml` の `created_at` だけで決まる。 + +**仮の名前へ控える理由**: 前の `snapshot.snar` を控えのコマンドに渡すと、tar は zstd の成否に関わらず snar を書き換え、 +zstd は既にある `full.tar.zst` を上書きしない。仮の名前と空の状態ファイルへ書けば、控えが失敗しても前のファイルは変わらない。 + +**入れ替えの順の理由**: 最初に snar を消すと、以後どこで止まっても次の `create` が snar の無い経路で作り直す。差分を +番号の大きい順に消すと、残るのは前の full から番号の小さい順に続く差分で、ある時点へ順に戻せる。`restore` は消すボリュームと +書き戻す起点を `meta.yml` から読むため、`full.tar.zst` と `meta.yml` が別の控えのものになる間は印を立てて復元を拒む +(`meta.yml` を消して印の代わりにすると、`snapshot_volumes` が旧レイアウトとして読み、別の組のボリュームを消す)。 +状態ファイルを最後に入れ替えるのは、印を外せずに止まった世代へ差分を積ませず、作り直しで印を外させるためである。 + +入れ替えの途中で止まった世代の状態: + +| 止まった所 | 復元で戻る時点 | 次の `create`(名前の指定・自動スナップショット) | +| --- | --- | --- | +| 3.1 の後(snar 無し) | 前の世代と同じ(前の full と全部の差分。`meta.yml` は前のまま) | snar の無い世代の経路で作り直す | +| 3.2 の途中(差分を減らした) | 前の full と、残った番号の小さい差分の時点(`meta.yml` は前のまま) | 同上 | +| 3.3 の後(印付き) | 復元しない(`meta.yml` は新しい組、`full.tar.zst` は前の組) | 同上 | +| 3.4 の後(新しい full) | 復元しない(印を外す前に止まった) | 同上 | +| 3.5 の後(印無し) | 新しい full の時点 | 同上 | + +復元は `incr-*.tar.zst` を glob で拾うため、`meta.yml` の `files` が前のままでも、残った差分だけを当てる。 常に成り立つ条件: - 控えのコマンドが失敗したら(手順 2 の定義: tar が 2 以上、または zstd が 0 以外)、仮の名前のファイルを消して `SnapshotError` を投げ直す。世代の既存のファイルは - 1 バイトも変わらず、ファイルは増えない + 1 バイトも変わらず、ファイルは増えない。`snapshot.yml` も変わらない +- 作り直しに成功した世代は、`full.tar.zst` と `snapshot.snar` が同じ 1 回の控えから作られ、`incr-*.tar.zst`・ + `snapshot.snar.bak`・仮の名前のファイルを持たない - 入れ替え(手順 3)の途中で止まった世代は `snapshot.snar` を持たない。次の `create`(名前の指定・自動 スナップショット)は snar の無い世代の経路で作り直す - 入れ替えの途中で止まった世代は、次のどちらかである @@ -292,12 +332,26 @@ classDiagram 小さい順に続く差分とともにある時点へ戻せる - `meta.yml` が `rebuilding: true` を持つ。`restore` はこの世代を、復元前バックアップとボリュームの消去の前に `SnapshotError` で止める +- 作り直しても `snapshot.yml` のエントリは 1 件のままで、`created_at` は変わらない +- 前のファイルを持たない世代の full の作成(新しい名前・`devbase up` の新しい世代・新しい復元前バックアップ)は、 + 控えのコマンド・アーカイブ・`meta.yml` がこの規則の前と同じで、作り直しの INFO を出さない +- 作り直しの間に `backups/<名前>/` が一時に使うディスクは、最大で前の世代の大きさと新しい full の大きさの和である + (仮の名前のファイルを同じディレクトリに置き、改名で入れ替える) - 作り直した後は、その世代から前の時点へ戻す手段は無い +入れ替えの途中で失敗したときは、成功として返さず `SnapshotError` で知らせる(「エラー処理」)。世代は上の表のどれかの状態で残り、 +同じ名前への `--full` か次の `create` で作り直せる。仮の名前のファイルを消せないときだけは「ファイルは増えない」が破れる。 +そのときは警告を出して残し、次の作り直しが控えの前に消す。 + +同じ秒に復元前バックアップが重なると、既にある `pre-restore-<時刻>` の世代を今の復元先の中身で作り直す。復元する世代が +その名前の世代そのものなら、展開の前に復元元が置き換わり、同じ秒に復元を 2 回打つと 1 回目の復元前バックアップが失われる +(#473)。作り直しが失敗したときは、ほかの復元前バックアップの失敗と同じく警告 1 行で控え無しの復元を続け、既にあった世代は消さない。 + ## データ・設定 **形は変えていない。** 移行は無く、変更前の devbase も同じファイルを読める。各世代の `meta.yml` に -足した `exclude`(#368)は、変更前の devbase が読み捨てる。 +足した `exclude`(#368)は、変更前の devbase が読み捨てる。作り直しの途中だけ `meta.yml` に立つ `rebuilding` も +キーを持たない世代は今のとおり読め、変更前の devbase は読み捨てる(印を見ずに復元する)。 | 保存先 | 系列の規則での扱い | | --- | --- | @@ -362,6 +416,12 @@ classDiagram `backups/` の外へ複製してから戻す - 別グループの起動を挟んで積んだ差分を実際の `devbase-snapshot` で復元し、最後の差分の時点の 共通ボリュームに戻ることは、自動テストでは確かめていない(tar は差し替えている) +- 世代の作り直しが入る前の devbase で、既にある世代の名前へ `--full` を打って失敗し、その後に同じ世代へ差分を + 積んだことがあれば、その世代の差分は失敗の前の変更を欠き得る(失敗した回に `snapshot.snar` だけが進んでいた)。 + ファイルの形からは見分けられないため、見つける処理は持たない。案内は `CHANGELOG.md` にあり、新しい世代を作り直す +- 差分の作成(`_create_incremental`)で控えのコマンドが失敗すると、`snapshot.snar` は進んだまま残る(#466)。 + 作り直しの経路と違い、差分の経路は前の状態へ戻さない。新しい世代へ直接控える full と差分の控えのコマンドは + `checked_backup_command` で包まず、tar の失敗を zstd の成功が隠し得る ## テスト観点 @@ -418,19 +478,35 @@ CLI と TUI: 世代の作り直し(`tests/snapshot/test_rebuild_generation.py`、`tests/snapshot/test_backup_real_tar.py`): -- 差分を持つ世代へ `--full` を打つと、`full.tar.zst`・`snapshot.snar`・`meta.yml` だけが残り、`meta.yml` が - full の形で今の除外の一覧を持ち、`snapshot.yml` のエントリが 1 件のまま `created_at` を変えないこと。 - 控えのコマンドが前の snar を受け取らないこと。控えのコマンドが tar の終了コード(`PIPESTATUS`)も見ること -- 実の GNU tar と zstd で、tar が読み取りで止まり(終了コード 2)zstd が成功しても、作り直しが `SnapshotError` で - 止まり、世代のファイル(前の full と差分)のバイト列が変わらないこと -- 控えのコマンドの失敗で世代のファイルのバイト列と `snapshot.yml` が変わらないこと。入れ替えの各段で - 止めると `SnapshotError` になり、snar と仮の名前のファイルが残らず、次の差分の指定が作り直すこと -- 別のボリュームの組で作り直して止めた世代は、印があれば `restore` が docker を 1 回も呼ばずに止まり、 - 印が無ければ `full.tar.zst` と `meta.yml` の組が合うこと。差分が番号の大きい順に消えること -- snar の無い世代への差分の指定と、同じ秒の復元前バックアップが作り直しになること。新しい世代の控えの - コマンドがこの規則の前と同じであること -- 実の GNU tar と zstd で、作り直した full がボリュームの全体を持ち、復元で全ファイルが作り直した時点の - 中身で戻り、その後の差分が後の変更だけを持つこと(GNU tar と zstd が無ければ飛ぶ) +- 実の GNU tar と zstd で、`a`・`b` の full と `incr-001` を持つ世代へ `c` を足して `create(name='g', full=True)` を打つと、 + `full.tar.zst` の中身の一覧に `a`・`b`・`c` がすべて入ること +- その世代を復元すると `a`・`b`・`c` が戻り、`a`・`b` の中身が作り直した時点の中身であること(前の `incr-001` の中身へ戻らないこと) +- 作り直した後の世代が `full.tar.zst`・`snapshot.snar`・`meta.yml` だけを持ち(`incr-*`・`snapshot.snar.bak`・仮の名前のファイルが無い)、 + `meta.yml` が `type: full`・`files: [full.tar.zst]`・`incremental_count: 0`・作り直した時点の除外の一覧を持ち、印を持たないこと +- `snapshot.yml` のその世代のエントリが 1 件のまま、`incremental_count` が 0 で、`created_at` が作り直す前と同じであること +- 控えのコマンドが前の snar を受け取らず仮の名前へ書き、tar の終了コード(`PIPESTATUS`)も見ること +- 控えのコマンドが失敗すると `SnapshotError`(CLI は終了コード 1)になり、世代のファイルのバイト列とディレクトリの一覧と + `snapshot.yml` が打つ前と同じであること。前の中断の残りの仮の名前のファイルは控えの前に消えること +- 実の GNU tar と zstd で、tar が読み取りで止まり(終了コード 2)zstd が成功しても、作り直しが `SnapshotError` で止まり、 + 前の full と差分のバイト列が変わらないこと +- 控えには成功し、入れ替えの各段で止めると `SnapshotError` になり、世代が snar と仮の名前のファイルを持たず、 + 次の差分の指定が作り直して印を外すこと。差分が番号の大きい順に消えること +- 別のボリュームの組で作り直して止めた世代は、印があれば `restore` が docker(復元前バックアップ・消去・展開)を 1 回も呼ばずに + `SnapshotError` で止まり、印が無ければ `full.tar.zst` と `meta.yml` の組が合って復元できること +- 作り直した後に `create(name='g')` を打つと `incr-001.tar.zst` ができ、実の GNU tar と zstd でその中身が作り直した後の変更だけであること +- `snapshot.snar` の無い世代(`incr-*` を持つ)への差分の指定が full へ切り替わり、前の `incr-*` が残らないこと +- 同じ秒に重なった復元前バックアップが既にある世代を作り直し、その名前を返すこと +- 作り直したときに、名前と除いた差分の本数を含む INFO が 1 行出ること +- 前のファイルを持たない世代の `create`(`--full` の有無とも)の控えのコマンド・`meta.yml`・`snapshot.yml` がこの規則の前と同じで、 + 作り直しの INFO を出さないこと + +実の GNU tar と zstd を使うテストは、どちらかが無ければ飛ぶ(CI の ubuntu では走る)。利用者の文書 +([スナップショットガイド](../user/snapshot-guide.md)・[CLI リファレンス](../user/cli-reference/05-snapshot.md))に、既にある世代の名前への +`--full` がその世代を今のボリュームの全体で作り直し、前の差分が消えて前の時点へは戻せないことが書かれていること。 + +実機で確かめる観点(作り直し): 作業用の名前の世代へ間にファイルを足して `devbase snapshot create --name <作業用> --full` を 2 回打ち、 +作業用のボリュームへ `snapshot restore` して全ファイルが戻ること。Linux の実機で、コンテナが root で書いた仮の名前のファイルを +ホストの利用者が改名・削除できること。 復元のテスト(`tests/snapshot/test_restore_incremental.py`、`test_manager_volumes.py` の復元)は 変えずに通ること。ただし `test_restore_incremental.py` の実の Docker で入れ替える場所は、既定の除外 diff --git a/issues/issue-458-design.md b/issues/issue-458-design.md deleted file mode 100644 index 6d368efd..00000000 --- a/issues/issue-458-design.md +++ /dev/null @@ -1,382 +0,0 @@ -# snapshot create: 既にある世代の名前で --full を打つと失敗し、その後の差分が失敗の前の変更を落とす → その世代を今のボリュームの全体で作り直し、失敗しても前の控えを保つ(#458) - -## 目的 - -- **何が壊れているか**: 既にある世代の名前で `devbase snapshot create --name <名前> --full` を打つと、`full.tar.zst` を作り直せずに失敗し、 - そのとき `snapshot.snar` だけが進む。その後に積んだ差分は、前の控えから失敗した回までの変更を含まない -- **誰が困るか**: 名前を付けて取り直す利用者(文書が `--name before-upgrade --full` を例に挙げる)。失敗の後に `devbase up` が同じ世代へ差分を積むと、 - その世代を復元したときにファイルが無いか古い中身になる -- **直すと何が成り立つか**: その世代は打った時点のボリュームの全体を 1 本の `full.tar.zst` で持ち、前の差分は残らない。作り直しに失敗しても、世代のファイルは打つ前と同じである - -## 適用範囲 - -- **働く範囲**: devbase の CLI(`lib/devbase/snapshot/manager.py`)。配布先の利用者の `backups/` の世代に効く。プラグインのリポジトリは触らない -- **プロジェクトごとに違うもの**: 無し(世代の形はプロジェクトに依らない。対象ボリュームの組と除外の一覧は今のとおり `--group` と `backups/exclude.yml` で受ける) -- **当たるモード**: `standard` - -## あるべき姿の根拠 - -| 根拠 | 種類 | 何を示すか | -| --- | --- | --- | -| `docs/user/cli-reference/05-snapshot.md` の `--full`「フルバックアップを強制作成」と例 `--name before-upgrade --full` | 外部の一次情報(利用者の文書) | 既にある名前でも、打った時点の全体を控えると約束している | -| `docs/user/snapshot-guide.md`「差分ではなく、強制的にフルバックアップを作成します」 | 外部の一次情報(利用者の文書) | 同上 | -| `devbase-snapshot:latest`(GNU tar 1.35・zstd 1.5.7)で、今のコマンドの形を作業用のディレクトリへ 2 回打った(2026-10-12) | 実測 | 2 回目は zstd が `already exists; stdin is an input - not proceeding.` で終了コード 1。`full.tar.zst` は前のまま、`snapshot.snar` は 76 → 80 バイトに進んだ。その後に打った差分の中身は `ai/` だけで、間に足した `ai/c` が無かった | - -要求と受け入れ条件は #458 の本文にある(コピーは `issues/issue-458-requirements.md`)。この文書は「どう作るか」だけを扱う。 - -課題の本文の「何を見つけたか」は、`full.tar.zst` が変更分だけで上書きされると書く。実際のコマンドの形(`tar -cf - | zstd -o`)では -zstd が上書きを拒み、上の実測の形になる。直す向きと受け入れ条件は変わらない。受け入れ条件 5(失敗しても世代のファイルを保つ)は、 -今の実装では `snapshot.snar` の点で破れている。 - -## ドメインモデル - -### コンテキスト - -| コンテキスト | 何の語が 1 つの意味に決まるか | -| --- | --- | -| スナップショット(`snapshot`) | 世代・系列・除外の一覧・復元前バックアップ、と今回足す「世代の作り直し」 | - -1 つのコンテキストで閉じる。 - -### 集約 - -| 集約 | 持ち主(書き換えてよいもの) | 根 | エンティティ | 値オブジェクト | -| --- | --- | --- | --- | --- | -| 世代 | `SnapshotManager._create_full`(作る・作り直す)と `_create_incremental`(差分を積む)。この変更で触るのは前者だけ | 世代のディレクトリ `backups/<名前>/` | — | `full.tar.zst`・`incr-NNN.tar.zst`・`snapshot.snar`・`meta.yml` | -| 世代の一覧 | `SnapshotManager._update_global_metadata` | `backups/snapshot.yml` | 世代のエントリ(`name` で識別) | `created_at`・`updated_at`・`incremental_count`・`volumes` | - -世代の一覧は世代を名前で参照する。作り直しは世代を書き終えてから一覧を更新する(今の順のまま)。 - -### 不変条件 - -| # | 集約 | 条件 | 破れたときの扱い | -| --- | --- | --- | --- | -| I1 | 世代 | 作り直しが成功した世代は、`full.tar.zst` と `snapshot.snar` が同じ 1 回の控えから作られ、`incr-*.tar.zst` と `snapshot.snar.bak` を持たない | 成功として返さない(I3) | -| I2 | 世代 | 作り直しの控えのコマンドが失敗したら、世代の既存のファイルはバイト列が打つ前と同じで、ファイルが増えない | `SnapshotError`。仮の名前のファイルを消してから返す | -| I3 | 世代 | 控えに成功した後、前の差分を除く・入れ替えるのに失敗したら `SnapshotError` を返す。途中のどの状態でも、世代は `snapshot.snar` を持たず、次のどちらかである: (a) `meta.yml` に作り直しの途中の印(`rebuilding: true`)が無く、`full.tar.zst` と `meta.yml` の対象ボリューム(`volumes`)・アーカイブの起点(`archive_root`)・除外の一覧(`exclude`)が同じ 1 回の控えのもので、ある時点へ順に戻せる組である。(b) `meta.yml` が印を持ち、`restore` はその世代を復元しない | 次の `create`(自動スナップショットを含む)が snar 無しの経路で full を作り直す。印のある世代の `restore` は、復元前バックアップとボリュームの消去の前に `SnapshotError` で止まる | -| I4 | 世代の一覧 | 作り直してもエントリは 1 件のままで、`created_at` は変わらない | 今の `_update_global_metadata` の既存エントリの更新で守る(この変更で触らない) | -| I5 | 世代 | 前のファイルを持たない世代の full の作成(新しい名前・`devbase up` の新しい世代・新しい復元前バックアップ)は、コマンド・アーカイブ・`meta.yml` が今と同じ | 退行。今のテストが落ちる | - -### ドメインイベント - -| # | イベント | 発生元 | 受け手 | -| --- | --- | --- | --- | -| E1 | 利用者が既存の世代の名前で `snapshot create --full` を打った | 利用者(`devbase snapshot create`) | `SnapshotManager.create` | -| E2 | 前の `snapshot.snar` を引き継がずに、ボリュームの全体を新しい full として仮の名前で控えた | `_create_full`(docker の中の tar・zstd) | `_create_full`(入れ替えへ進む) | -| E3 | 前の差分アーカイブと前の状態の控えを世代から除き、仮の名前のファイルを本来の名前へ入れ替えた | `_install_rebuilt_full` | `_create_full`(INFO を出す) | -| E4 | 世代の `meta.yml` を full の形で書いた(作り直しでは E3 の中で、印付き → 印無しの 2 回に分けて書く) | `_create_full`(新しい世代)・`_install_rebuilt_full`(作り直し) | `create`(一覧の更新へ進む) | -| E5 | `snapshot.yml` のその世代の `incremental_count` と `updated_at` を更新した | `_update_global_metadata` | `restore`・`auto_snapshot_target`・`rotate`(読む側) | -| E6 | 利用者がその世代を復元し、ボリュームが E2 の時点の中身になった | 利用者(`devbase snapshot restore`) | 利用者 | -| E7 | 次の `devbase up` の自動スナップショットが、その世代へ `incr-001` を積んだ | `_auto_snapshot` → `create(name)` | `_create_incremental` | - -E3 は要求の E3(差分を除く)に、仮の名前からの入れ替えを含めた。入れ替えを先にすると、除けなかったときに新しい full へ前の差分が当たるためである(決定 2)。 - -### 用語 - -| 用語 | 意味 | 用語集への反映 | -| --- | --- | --- | -| 世代の作り直し | 既にある世代のディレクトリへ、前の `snapshot.snar` を引き継がずに新しい full を作り、前の差分と状態のファイルを除くこと。`--full` を既存の名前へ打ったとき・snar の無い世代への差分の指定・同じ秒に重なった復元前バックアップで起きる | 追加(`snapshot`) | -| 世代 | `backups/<名前>/` の 1 つ。`full.tar.zst` 1 つと、0 個以上の `incr-NNN.tar.zst` からなる | 変更なし | -| 復元前バックアップ | `restore` が書き戻す前に自動で作るフルの世代 `pre-restore-<時刻>` | 変更なし | -| 除外の一覧 | スナップショットのアーカイブに入れないパスの一覧 | 変更なし | - -## 機能一覧 - -| # | 機能 | 誰が使うか | -| --- | --- | --- | -| F1 | 既にある世代を `--full` で今のボリュームの全体に作り直す | `devbase snapshot create --name <既存> --full` を打つ利用者 | -| F2 | `snapshot.snar` の無い世代への差分の指定を、作り直しとして full に切り替える | `create --name <既存>` を打つ利用者と `devbase up` の自動スナップショット | -| F3 | 同じ秒に重なった復元前バックアップを、作り直しとして作る | `devbase snapshot restore` を打つ利用者 | -| F4 | 作り直しに失敗しても前の控えを保ち、失敗を終了コード 1 で知らせる | F1〜F3 の利用者 | -| F5 | 作り直したことを INFO の 1 行で知らせる | F1〜F3 の利用者 | -| F6 | 既存の名前への `--full` の結果を利用者の文書に書く | 利用者 | - -## 構成要素 - -| 要素 | 責務 | -| --- | --- | -| `SnapshotManager._create_full`(変更) | 世代に前のファイル(`full.tar.zst`・`snapshot.snar`・`incr-*.tar.zst` のどれか)があれば仮の名前で控え、新しい控えの `meta.yml` の内容を組んで `_install_rebuilt_full` へ渡す。無ければ今のとおり本来の名前で控え、最後に今と同じ形の `meta.yml` を書く | -| `SnapshotManager.backup_command`(変更) | 状態ファイルの名前を引数 `snar`(既定 `snapshot.snar`)で受ける。既定のままなら今と同じ文字列を返す | -| `SnapshotManager._install_rebuilt_full`(追加) | 控えの成功の後に、前の状態ファイルと差分を除き、`meta.yml` を印付きの新しい内容へ置き換え、仮の名前の full を本来の名前へ入れ替え、`meta.yml` を印無しへ置き換え、最後に仮の名前の状態ファイルを入れ替える(決定 2)。`meta.yml` の置き換えは仮の名前 `meta.yml.new` へ書いて `os.replace` する。除いた差分の本数を返す。途中で失敗したら `SnapshotError` | -| `SnapshotManager.restore`(変更) | 書き戻す組を読む前に、世代の `meta.yml` が印(`rebuilding: true`)を持てば `SnapshotError` で止める。復元前バックアップもボリュームの消去も行わない | -| `SnapshotManager._discard_rebuild_temps`(追加) | 仮の名前のファイル(3 本)を消す。控えの前(前の中断の残り)と失敗の後に呼ぶ。消せなければ警告して続ける | -| 定数 `FULL_ARCHIVE_TMP`・`SNAR_FILE_TMP`・`META_FILE_TMP`(追加) | 仮の名前 `full.tar.zst.new`・`snapshot.snar.new`・`meta.yml.new`。`is_archive_file` と `incr-*.tar.zst` の glob に当たらない | -| `create`・`_create_incremental`・`_backup_before_restore`・`_update_global_metadata`(変更なし) | 今のとおり `_create_full` を呼び、その後に一覧を更新する。作り直しは `_create_full` の中で決まる | -| `docs/user/cli-reference/05-snapshot.md`・`docs/user/snapshot-guide.md` | 既存の名前への `--full` の結果(作り直し、前の差分は消え前の時点へは戻せない) | -| `docs/specifications/snapshot-series.md` | 「世代の作り直し」の節(手順・失敗の扱い・ログ)。`snapshot-exclude.md` の運用の節の #458 の行を書き換える | -| `CHANGELOG.md` の `[Unreleased]`・`docs/glossary/glossary.json`・`docs/glossary.md` | 利用者に見える変更(決定 5 の案内を含む)と、語「世代の作り直し」 | - -```mermaid -graph LR - subgraph CLI - C[snapshot create] - U[devbase up の自動スナップショット] - R[snapshot restore] - end - subgraph SnapshotManager - CR[create] - CI[_create_incremental] - BR[_backup_before_restore] - RS[restore] - CF[_create_full] - BC[backup_command] - IN[_install_rebuilt_full] - DT[_discard_rebuild_temps] - UG[_update_global_metadata] - end - D[(docker の中の tar・zstd)] - G[(backups/名前/)] - Y[(backups/snapshot.yml)] - C --> CR - U --> CR - R --> RS - RS --> BR - RS -->|印を見る| G - CR --> CF - CR --> CI - CI --> CF - BR --> CF - CF --> BC - CF --> D - CF --> IN - CF --> DT - D --> G - IN --> G - DT --> G - CR --> UG - BR --> UG - UG --> Y -``` - -図は処理の要素だけを描き、定数(仮の名前)と文書・`CHANGELOG.md`・用語集を含めない。`_create_full` を呼ぶのは `create`(新しい名前・`--full`)、`_create_incremental`(snar 無しの切り替え)、`_backup_before_restore` の 3 つで、ほかに無い -(`grep -n "_create_full" lib/` で `manager.py` の 3 か所)。 - -## 構造 - -触る型は `SnapshotManager` だけで、型の関係は変わらない。触るメソッドと定数を示す。 - -```mermaid -classDiagram - class SnapshotManager { - +create(name, full) str - +restore(name, point) - +backup_command(archive, volumes, excludes, snar)$ str - -_create_full(name, snap_dir, volumes) - -_install_rebuilt_full(snap_dir, meta) int - -_discard_rebuild_temps(snap_dir) - -_update_global_metadata(name, snap_dir) - } - class SnapshotError - SnapshotManager ..> SnapshotError : 失敗で投げる -``` - -## データ構造 - -`snapshot.yml` の形は変えない。`meta.yml` は作り直しの途中だけ真偽値のキー `rebuilding: true` を持ち得る(作り直しを終えた世代と新しい世代は持たない)。 -キーを持たない今の世代は今のとおり読めるため、移行は無い。ほかは世代のディレクトリに置くファイルの扱いだけが変わる。 - -| ファイル | 今 | 作り直しの後 | -| --- | --- | --- | -| `full.tar.zst` | 作り直せない(zstd が上書きを拒む) | 新しい控え | -| `snapshot.snar` | 失敗した回の状態へ進む | 新しい控えの状態(前の snar を引き継がない) | -| `incr-*.tar.zst` | 残る | 無い | -| `snapshot.snar.bak` | 残る | 無い | -| `meta.yml` | 前のまま(失敗のため) | `type: full`・`files: [full.tar.zst]`・`incremental_count: 0`・今の `exclude`・`volumes`・`created_at`(作り直した時刻) | -| `meta.yml` の `rebuilding` | 無い | 作り直しの途中(新しい控えの内容を書いてから印を外すまで)だけ `true`。作り直した後は無い | -| `full.tar.zst.new`・`snapshot.snar.new`・`meta.yml.new` | 無い | 作り直しの間だけある。成功でも失敗でも残さない | -| `snapshot.yml` のエントリ | 前のまま | `incremental_count: 0`・`updated_at` を更新、`created_at` は前のまま | - -`meta.yml` の `created_at` は今の `_create_full` のとおり作り直した時刻を書く。系列の新旧とローテーションは `snapshot.yml` の `created_at` -だけで決まり(`snapshot-series.md` の「データ・設定」)、`meta.yml` の `created_at` を読む処理は無い。 - -| 機能 | `full.tar.zst` | `incr-*` | `snapshot.snar`(`.bak`) | 仮の名前のファイル | `meta.yml` | `snapshot.yml` | -| --- | --- | --- | --- | --- | --- | --- | -| F1〜F3 の成功 | 更新 | 削除 | 更新(`.bak` は削除) | 作成 → 改名 | 更新 | 更新 | -| F4(控えの失敗) | — | — | — | 作成 → 削除 | — | — | -| F4(除く・入れ替えの失敗) | 止まった所により更新 | 削除(止まった所まで) | 削除 | 削除 | 止まった所により印付き・印無しで更新 | — | - -## 入出力の契約 - -`devbase snapshot create` の引数・終了コードは変えない。`--full` を既存の名前へ打った結果と、出す文言だけが変わる。 - -| 場面 | 結果 | 出力 | -| --- | --- | --- | -| 作り直しに成功 | 終了コード 0 | INFO `フルバックアップを作成中: {名前}`(今の行)の後に INFO `世代 {名前} を今のボリュームの全体で作り直しました(前の差分 {本数} 本を除きました)` | -| 控えのコマンドが失敗 | `SnapshotError` → 終了コード 1 | 今の `Dockerでのtar操作に失敗しました: {stderr}`。世代は打つ前のまま | -| 除く・入れ替えに失敗 | `SnapshotError` → 終了コード 1 | `世代 '{名前}' の作り直しを終えられませんでした: {理由}。もう一度 devbase snapshot create --name {名前} --full を打ってください` | -| 前のファイルを持たない世代の full | 今と同じ | 今と同じ(作り直しの行を出さない) | -| 作り直しの途中の印を持つ世代の `restore` | `SnapshotError` → 終了コード 1。復元前バックアップを作らず、ボリュームを消さない | `世代 '{名前}' は作り直しの途中で止まっているため復元できません。devbase snapshot create --name {名前} --full を打って作り直してください` | - -復元前バックアップ(F3)の失敗は、今のとおり `_backup_before_restore` が警告 1 行に変えて復元を続ける。既にあった同名のディレクトリは消さない(今の振る舞い)。 - -## 処理の流れ - -```mermaid -sequenceDiagram - participant M as _create_full - participant D as docker の中の tar・zstd - participant F as backups/名前/ - M->>F: 前のファイル(full・snar・incr)があるかを見る - alt 前のファイルが無い - M->>D: backup_command(full.tar.zst, snar=snapshot.snar) - D-->>M: 成功 / SnapshotError(今と同じ) - else 前のファイルがある(作り直し) - M->>F: _discard_rebuild_temps(前の中断の残りを消す) - M->>D: backup_command(full.tar.zst.new, snar=snapshot.snar.new) - alt 控えが失敗 - D-->>M: SnapshotError - M->>F: _discard_rebuild_temps - M-->>M: SnapshotError を投げ直す(世代は前のまま) - else 控えが成功 - M->>F: _install_rebuilt_full(meta.yml も書く) - alt 除く・入れ替えが失敗 - M->>F: _discard_rebuild_temps - M-->>M: SnapshotError(作り直しを終えられなかった) - else 成功 - M-->>M: INFO(名前・除いた本数) - end - end - end - opt 前のファイルが無い - M->>F: meta.yml を full の形で書く(今と同じ) - end -``` - -`_install_rebuilt_full` は次の順で 1 つずつ行う。どこで止まっても、世代は `snapshot.snar` を持たず、「印が無く、`full.tar.zst` と `meta.yml` が同じ控えのもので、 -ある時点へ順に戻せる組」か「`meta.yml` が印を持ち、復元しない組」のどちらかである(I3・決定 2)。 - -```mermaid -stateDiagram-v2 - [*] --> 前の世代: 控えに成功(仮の名前の 2 本がある) - 前の世代 --> snar無し: snapshot.snar.bak と snapshot.snar を消す - snar無し --> 差分を減らした: incr を番号の大きい順に消す - 差分を減らした --> 差分を減らした: 次の incr を消す - 差分を減らした --> 印付き: meta.yml を新しい控えの内容と rebuilding true へ置き換える - snar無し --> 印付き: incr が無い - 印付き --> 新しいfull: full.tar.zst.new を full.tar.zst へ改名 - 新しいfull --> 印無し: meta.yml を新しい控えの内容(印無し)へ置き換える - 印無し --> 作り直した: snapshot.snar.new を snapshot.snar へ改名 - 作り直した --> [*] -``` - -| 止まった状態 | 復元で戻る時点 | 次の `create`(名前の指定・自動スナップショット) | -| --- | --- | --- | -| snar 無し | 前の世代と同じ(前の full と全部の差分。`meta.yml` は前のまま) | snar 無しの経路で作り直す(F2) | -| 差分を減らした | 前の full と残った番号の小さい差分の時点(`meta.yml` は前のまま) | 同上 | -| 印付き | 復元しない(`meta.yml` は新しい組、`full.tar.zst` は前の組で、合わない) | 同上 | -| 新しい full | 復元しない(両方とも新しい組だが、印を外す前に止まった) | 同上 | -| 印無し | 新しい full の時点 | 同上 | - -## 非機能の実現方式 - -| 大項目 | 要求の条件 | 実現方式 | 確かめ方 | -| --- | --- | --- | --- | -| 可用性 | 作り直しに失敗しても、その世代は作り直す前と同じに復元できる(受け入れ条件 5) | 新しい控えは仮の名前へ書き、前の `snapshot.snar` を tar に渡さない。控えが成功するまで前のファイルに触らない | 控えのコマンドを失敗させ、前の 4 ファイルのバイト列と一覧が同じことを見る(受け入れ条件 5) | -| 性能・拡張性 | 一時に要るディスクは最大で「前の世代の大きさ + 新しい full の大きさ」で、これを超える置き場を足さない | 仮の名前のファイルは同じ世代のディレクトリに置き、改名で入れ替える(写さない)。前の差分は入れ替えの前に消す | 実装で写す処理(`shutil.copy*`)を使っていないことをレビューで見る。大きさは測らない | -| 運用・保守性 | 作り直したことが INFO の 1 行で分かる(世代の名前と、除いた差分の本数を含む) | `_install_rebuilt_full` の成功の後に `_create_full` が INFO を 1 行出す | ログを捕まえ、名前と本数を含む 1 行があることを見る | - -## 決定の記録 - -### 決定 1: 作り直しで前の控えを失わないため、新しい full を仮の名前と新しい状態ファイルで作ってから入れ替える - -控えのコマンドに前の `snapshot.snar` を渡すと、tar は zstd の成否に関わらず snar を書き換える(実測)。仮の名前 -`full.tar.zst.new` と新しい状態ファイル `snapshot.snar.new` へ書けば、控えが失敗しても前のファイルは 1 バイトも変わらず、 -仮の 2 本を消すだけで打つ前に戻る。新しい状態ファイルは空から始まるため、tar はレベル 0(全体)で控える。入れ替えはホストの -`os.replace` で同じディレクトリの中の改名にし、写さない(一時のディスクを要求の上限に収める)。 - -採らなかった形: 前の snar を消してから同じ名前へ控える形は、控えが失敗すると前の snar を失い、zstd も既存の `full.tar.zst` を -上書きしない。zstd に `-f` を足して上書きさせる形は、失敗した控えが前の full を壊す。 - -根拠: Value 2(MVV 版 1) - -### 決定 2: 入れ替えの途中で止まっても誤った中身を復元しないため、状態ファイル → 差分(番号の大きい順)→ 印付きの meta.yml → full → 印無しの meta.yml → 状態ファイルの順で入れ替える - -前の差分が新しい full の上に当たると、変わっていないファイルが前の中身へ戻り、差分の削除の記録で新しいファイルが消える。最初に -`snapshot.snar` を消すと、以後どこで止まっても世代は snar を持たず、次の `create` が今ある snar 無しの経路で作り直す。差分を番号の -大きい順に消すと、残るのは前の full から番号の小さい順に続く差分で、ある時点へ順に戻せる組になる。 - -`restore` は消すボリュームと書き戻す起点を `meta.yml` の `volumes`・`archive_root` から読む。別のボリュームの組で作り直すと、`full.tar.zst` と -`meta.yml` の入れ替えの間は 2 つが別の組を指し、そこで止まった世代を復元すると、前の組のボリュームを消して新しい組のデータを書き戻す。 -2 つのファイルを 1 回の改名で同時に入れ替える手段は無いため、合わない間は `meta.yml` に印 `rebuilding: true` を立て、`restore` が -消去の前に拒む。印を立てる前は前の full と前の `meta.yml`、印を外した後は新しい full と新しい `meta.yml` が揃う。`meta.yml` の書き換えは -仮の名前へ書いてから改名し、途中まで書いた `meta.yml` を残さない。状態ファイルを最後に入れ替えるのは、印を外せずに止まった世代へ -次の `create` が差分を積まず、snar 無しの経路で作り直して印を外すためである。 - -採らなかった形: full を先に入れ替える形は、差分を除けなかったときに前の差分が新しい full へ当たる。差分を番号の小さい順に消す形は、 -残った大きい番号の差分が飛んだ番号の差分を前提にして当たる。`meta.yml` を最後に 1 回だけ書く形は、full の入れ替えの後に止まると -新しい full と前の `meta.yml` が残り、前の組のボリュームを消して別の組のデータを書き戻す。`meta.yml` を先に消して印の代わりにする形は、 -`meta.yml` の無い世代を `snapshot_volumes` が旧レイアウト(`devbase_home_ubuntu` をルートへ)として読み、同じ取り違えが起きる。 - -根拠: Value 2(MVV 版 1) - -### 決定 3: 新しい世代の作成を今と同じに保つため、作り直しの手順は世代に前のファイルがあるときだけ通す - -前のファイル(`full.tar.zst`・`snapshot.snar`・`incr-*.tar.zst` のどれか)が無ければ、今のとおり本来の名前へ直接控える。 -新しい世代の控えのコマンド・アーカイブ・`meta.yml` は 1 文字も変わらず、受け入れ条件 9 と今のテスト(コマンドの文字列を比べる -`test_archive_layout.py` を含む)がそのまま通る。判定は `_create_full` の中に置き、呼び出し元 3 つ(F1〜F3)は変えない。 - -採らなかった形: すべての full を仮の名前で作る形は、規則が 1 つになるが、新しい世代の控えのコマンドが変わり、今のテストの期待を書き換えることになる。 -`create` の `is_new` で分ける形は、snar 無しの切り替え(F2)と同じ秒の復元前バックアップ(F3)を拾えない。 - -根拠: Value 2(MVV 版 1) - -### 決定 4: 既存の名前への `--full` を止めずに受け付け、前の差分は消して前の時点へは戻せないと文書に書く - -要求の前提 1・2 のとおり。文書が `--name before-upgrade --full` を例に挙げ、同じ名前で取り直す使い方を禁じていない。前の時点を残したい -利用者は別の名前で作るか、作り直す前に `snapshot copy` で写す。文書の書き先は `05-snapshot.md` の `--name` の説明の後の段落と、 -`snapshot-guide.md` の「名前付きフルバックアップ」の例の後である。 - -採らなかった形: 既存の名前を拒んで新しい名前を求める形は、文書の例と利用者の取り直しの使い方を壊す。 - -根拠: Value 2(MVV 版 1) - -### 決定 5: すでに傷んだ世代を見つける処理は足さず、`CHANGELOG.md` で条件と取り直しを案内する(要求の未決 1 の案) - -実測では、今の実装は `full.tar.zst` を差分で上書きしない(zstd が拒む)。要求の未決 1 が想定する「`full.tar.zst` が差分で `incr-*` が残る世代」は -この経路では生じない。実際に生じる傷みは「失敗した回の後に積んだ差分が、その前の変更を落とす」で、世代のファイルの形(`meta.yml`・ -ファイルの一覧・mtime)から区別できない。見つける処理を `list` や `restore` に足しても当たる世代を示せないため、`CHANGELOG.md` に -「既存の世代の名前へ `--full` を打って失敗したことがあり、その後に同じ世代へ差分が積まれていれば、その世代の差分は失敗の前の変更を -欠き得る。新しい世代を作り直す(`devbase snapshot create`)」と書く。 - -採らなかった形: `list` に警告の列を足す形は、判定の材料が無く、どの世代にも出すか出さないかの 2 つしか取れない。 - -根拠: Value 2(MVV 版 1) - -## テスト設計 - -| 受け入れ条件・不変条件 | どの振る舞いで縛るか | どう壊したら落ちるべきか | -| --- | --- | --- | -| 受け入れ条件 1 | 実の GNU tar と zstd で、`a`・`b` の full と `incr-001` を持つ世代へ `c` を足して作り直すと、`full.tar.zst` の一覧に `a`・`b`・`c` がある | 前の snar を tar へ渡す・仮の名前を使わず zstd に上書きさせない、と落ちる | -| 受け入れ条件 2 | 同じ世代を実の tar で復元すると `a`・`b`・`c` があり、`a`・`b` は作り直した時点の中身である | `incr-001` を残すと(前の中身へ戻る・消える)落ちる | -| 受け入れ条件 3・I1 | 作り直した後の世代に `incr-*`・`snapshot.snar.bak`・仮の名前のファイルが無く、`meta.yml` が full の形で今の除外の一覧を持つ | 差分を消さない・`.bak` を残す・`meta.yml` の `exclude` を前の記録から取る、と落ちる | -| 受け入れ条件 4・I4 | `snapshot.yml` の `g` が 1 件で、`incremental_count` が 0、`created_at` が作り直す前と同じ | エントリを足し直す・`created_at` を書き換える、と落ちる | -| 受け入れ条件 5・I2 | 控えのコマンドが失敗すると `SnapshotError` になり、4 ファイルのバイト列とディレクトリの一覧と `snapshot.yml` が打つ前と同じ | 前の snar を tar に渡す・失敗の後に仮の名前のファイルを消さない・先に差分を消す、と落ちる | -| 受け入れ条件 6・I3 | 控えは成功し、差分を消す・`meta.yml` を書く・改名するところで失敗させると `SnapshotError` になる。止まった世代は snar を持たず、次の `create(name='g')` が full を作り直して印を外す | 消す失敗を握りつぶす・snar を最初に消さない・番号の小さい順に消す・snar を印を外す前に入れ替える、と落ちる | -| I3(組の整合) | 前の世代と別のボリュームの組で作り直し、full の改名の前後・印を外す前で止めると、`meta.yml` が印を持ち、`restore('g')` が `SnapshotError` になって docker(復元前バックアップ・消去・展開)を 1 回も呼ばない。印を外した後に止めた世代は新しい組で復元する | 印を立てない・full の後で `meta.yml` を 1 回だけ書く・`restore` が印を見ずに消去へ進む、と落ちる | -| 受け入れ条件 7 | 実の tar で、作り直した後に `d` を足して `create(name='g')` を打つと `incr-001.tar.zst` ができ、中身は `d` だけ | 新しい snar を本来の名前へ入れ替えない・前の snar を残す、と落ちる | -| 受け入れ条件 8 | snar の無い世代(`incr-*` を持つ)へ `create(name='g')` を打つと full へ切り替わり、`incr-*` が残らない | 作り直しの判定を `full=True` の経路だけに置く、と落ちる | -| 受け入れ条件 9・I5 | 新しい名前の `create`(`--full` の有無)・自動スナップショット・新しい復元前バックアップの控えのコマンドと `meta.yml` と `snapshot.yml` が今と同じ。`tests/snapshot/`・`tests/commands/` が通る | すべての full を仮の名前で作る・作り直しの INFO を新しい世代でも出す、と落ちる | -| 受け入れ条件 10 | `05-snapshot.md` と `snapshot-guide.md` に、既存の名前への `--full` が世代を作り直し、前の差分が消え前の時点へは戻せないことがある | 文書の段落を書かないと、文書の差分のレビューで落ちる(自動のテストは置かない) | -| 運用・保守性(INFO) | 作り直しで、名前と除いた本数を含む INFO が 1 行出る | 本数を数えない・行を出さない、と落ちる | -| 同じ秒の復元前バックアップ(F3) | 既にある `pre-restore-<時刻>` へ作り直すと、復元前バックアップが作られて名前が返る(今は失敗して `None`) | 作り直しを `create` の経路だけに置く、と落ちる | - -受け入れ条件 1・2・7 は `tests/snapshot/test_backup_real_tar.py` の形(実の tar・zstd。無ければ飛ぶ)で、ほかは docker を差し替えたテストで縛る。 -CI(ubuntu)で飛ばずに走ったことを、実装の Pull Request の CI の記録で確かめる(要求の前提 4)。 - -## 設計の結果 - -| 課題 | 扱い | 取り込み先 | 触るファイル | -| --- | --- | --- | --- | -| #458 | 実装する | — | `lib/devbase/snapshot/manager.py`、`tests/snapshot/`、`docs/user/cli-reference/05-snapshot.md`、`docs/user/snapshot-guide.md`、`docs/specifications/snapshot-series.md`、`docs/specifications/snapshot-exclude.md`、`CHANGELOG.md`、`docs/glossary/glossary.json`、`docs/glossary.md` | - -## 未確認のまま残ること - -| 項目 | 内容 | -| --- | --- | -| 要求の本文と実測の差 | 課題の本文の「何を見つけたか」は上書きの形を書く。実測は失敗と snar の進みである(あるべき姿の根拠)。本文を直すかは承認ゲート 1 で利用者が決める。受け入れ条件は変わらない | -| 未決 1 の扱い | 決定 5 の案(見つける処理を足さず `CHANGELOG.md` で案内)を承認ゲート 1 で利用者が決める | -| 仮の名前のファイルを消せないとき | `_discard_rebuild_temps` は警告して続けるため、そのときは受け入れ条件 5 の「ファイルが増えない」が守れない。同じディレクトリに作ったファイルを消せない場面は確かめていない。次の作り直しが控えの前に消す | -| Linux でのファイルの持ち主 | コンテナが root で書いた仮の名前のファイルを、ホストの利用者が改名・削除する。ディレクトリの書き込みの権限で足り、`delete` の `shutil.rmtree` が同じ形で動いている。Linux の実機での作り直しはリリース後テストで見る | -| 差分の経路の失敗 | `_create_incremental` で控えが失敗すると snar が進んだまま残る。範囲外として #466 に残した | -| 控えのパイプの終了コード | `tar ... | zstd` は `pipefail` を持たず、tar の失敗を zstd の成功が隠す。今の新しい世代の作成と同じ振る舞いで、この変更では変えない | -| 実機の確かめ | 作業用の名前の世代へ `--full` を 2 回打ち、作業用のボリュームへ復元して全ファイルが戻ることは、リリース後テストで見る(要求の検証手段) | diff --git a/issues/issue-458-requirements.md b/issues/issue-458-requirements.md deleted file mode 100644 index f7f94945..00000000 --- a/issues/issue-458-requirements.md +++ /dev/null @@ -1,194 +0,0 @@ -# #458: 既存の世代の名前で snapshot create --full を打つと、full.tar.zst が変更分だけになり復元でファイルを失う - -正は課題の本文(#458)で、この文書はその写しである。`spec-copy.py write` で作り直す。手で直さない。 - -## 何を見つけたか - -既にある世代の名前で `devbase snapshot create --name <名前> --full` を打つと、`full.tar.zst` が「前の控えからの変更分」だけのアーカイブになる。`_create_full` は世代のディレクトリにある `snapshot.snar` を消さずに `tar --listed-incremental=/backup/snapshot.snar` を打つため、GNU tar は差分(レベル 1)として控える。 - -`devbase-snapshot:latest` の GNU tar で、同じコマンドの形を使い確かめた(2026-10-12、作業用のディレクトリ。利用者のボリュームは使っていない)。 - -1. `ai/a`・`ai/b` を置き、`tar --listed-incremental=/backup/snapshot.snar -cf /backup/full.tar -C /source ai` -2. `ai/c` を足し、同じ snar のまま同じコマンドで `full.tar` を作り直す -3. `tar -tf /backup/full.tar` → `ai/` と `ai/c` だけ(`ai/a`・`ai/b` が無い) - -あわせて、前からあった `incr-*.tar.zst` は消されずに残り、`meta.yml` の `files` だけが `[full.tar.zst]` に戻る(復元は `incr-*.tar.zst` を glob で拾うため、残った差分も適用される)。 - -## どこで見つけたか - -`lib/devbase/snapshot/manager.py` の `create`(`is_new` が偽で `full=True` のとき)→ `_create_full` → `backup_command`。#368(スナップショットの除外)の設計で、除外を変えた後に利用者が既存の世代へ `--full` を打つ経路を調べていて見つけた。 - -## なぜこの変更の範囲外なのか - -#368 の受け入れ条件は、除外の適用と、除外の一覧が違う世代へ差分を積まないこと(受け入れ条件 6・7)までで、既存の世代を `--full` で作り直す経路の振る舞いを含まない。対象範囲の「含まない」にある「復元の対象・順序・失敗時の案内の変更」「世代の保持・系列の規則の変更」にも当たらない別の不具合で、除外の有無に関わらず起きる。根拠: Value 2(MVV 版 1) - -## 直さないと何が起きるか - -既存の世代の名前で `--full` を打った後にその世代を復元すると、復元はボリュームを空にしてから `full.tar.zst` を展開するため、前の控えから変わっていないファイル(多くは会話の記録や設定)が復元後のボリュームに無い。利用者は「フルを取り直した」と思っているため、失ったことに復元の後まで気づかない。`--name` を省いた `create` と `devbase up` の自動スナップショット(新しい世代は新しいディレクトリ)では起きない。 - -## 起票者の推定 - -- 利用者が既存の世代の名前で `--full` を打つことがある(推した出所: `docs/user/cli-reference/05-snapshot.md` の `--name` と `--full` の説明が、既存の名前を禁じていない) -- 復元で変わっていないファイルを失う(推した出所: 上の手順 3 の中身の一覧と、`manager.py` の `restore` が `clear_command` の後に full を展開すること。復元までは実際に試していない) - -## 由来 - -issue #368 - -## 依頼(原文) - -上の「何を見つけたか」から「由来」までの節が、この課題の依頼の原文である。 - -(書き手: 起票者、日付: 2026-10-12。#368 の設計の途中で AI が見つけて起票した本文で、利用者の指示の原文ではない。 -マイルストーン「04 CLI 引数と文書」の説明にある「実害の観測があり自動で前倒しした課題」も起票者・棚卸しの記録である) - -> #458。人へ問わずに進め、決められない点は前提か未決として課題の本文へ書く。 - -(書き手: AI の起動指示、日付: 2026-10-12) - -利用者の指示の原文は無い。直す向き(`--full` は既存の世代でもその時点のボリューム全体を控える)は、 -`docs/user/cli-reference/05-snapshot.md` の `--full`「フルバックアップを強制作成」と、`docs/user/snapshot-guide.md` の -「差分ではなく、強制的にフルバックアップを作成します」から読んだ。文書が約束している振る舞いに実装を合わせる変更である。 - -## 起票者の推定の扱い - -上の「起票者の推定」の節の 2 行を写し、この要求での扱いを書き足す。承認ゲート 1 で利用者が確かめるまで、利用者の決定として扱わない。 - -| # | 条件 | 推した出所 | この要求での扱い | -| --- | --- | --- | --- | -| S1 | 利用者が既存の世代の名前で `--full` を打つことがある | `docs/user/cli-reference/05-snapshot.md` の `--name` と `--full` の説明が、既存の名前を禁じていない | 前提 1。打つことの有無に関わらず、文書が禁じていない操作として直す | -| S2 | 復元で変わっていないファイルを失う | 本文の手順 3 と、`manager.py` の `restore` が `clear_command` の後に full を展開すること(起票の時点で復元は試していない) | 受け入れ条件 1・2(作り直した後の復元で全ファイルが戻ることを、実の GNU tar で確かめる) | - -## 目的 - -- 既存の世代の名前で `devbase snapshot create --name <名前> --full` を打ったとき、その世代が「打った時点のボリュームの - 全体(除外の一覧を除く)」を 1 本の `full.tar.zst` で持つ状態にし、復元でファイルを失わないようにする -- 作り直しに失敗しても、それまでの世代の控えを失わない(Value 2「作り直しで失わない」) - -## 前提 - -- 前提 1: 既存の世代の名前で `--full` を打つ操作は禁じず、受け付けて世代を作り直す(止めて新しい名前を求める形は採らない。 - 文書が「名前付きフルバックアップ」の例として `--name before-upgrade --full` を挙げ、同じ名前で取り直す使い方を禁じていないため) -- 前提 2: 作り直した世代は、前の `full.tar.zst`・`incr-*.tar.zst` の中身を引き継がない。作り直しが成功した後、 - その世代から前の時点へ戻す手段は無くなる(前の時点を残したい利用者は別の名前で作る。今の文書の範囲で読める) -- 前提 3: `snapshot.yml` のその世代の `created_at` は今の振る舞いのまま変えない(差分を積んだときと同じく `updated_at` と - `incremental_count` だけを更新する)。系列の最新の世代とローテーションの順は `created_at` で決まるため、変えると - 別の課題の振る舞いが変わる -- 前提 4: CI の `pytest`(ubuntu の runner)には GNU tar と zstd があり、実の tar で走るテスト(`tests/snapshot/test_backup_real_tar.py` - の形)が飛ばされずに走る。macOS の手元では飛ばされ得るため、実の tar の確かめは CI の結果か `devbase-snapshot:latest` の中で取る -- 前提 5: 「作り直しに失敗した」は、控えのコマンド(docker の中の tar・zstd)が失敗して `SnapshotError` になった場合を指す。 - ホストのプロセスが途中で殺された・電源が落ちた場合の中途の状態は、この課題では扱わない - -## 対象範囲 - -含む: - -- 既存の世代のディレクトリへ `full.tar.zst` を作る経路の振る舞い(`create --name <既存> --full`)。同じ関数を通る - ほかの経路(`snapshot.snar` が無い世代への差分の指定が full へ切り替わる経路、同じ秒に重なった復元前バックアップ)も、 - 作り直した世代に前の差分が残らない同じ規則に従う -- 作り直しの後の世代の `meta.yml`(`files`・`incremental_count`・`exclude`・`type`)と `snapshot.yml` のエントリの値 -- 作り直しに失敗したときに、世代のファイルを作り直す前の状態に保つこと -- この振る舞いを利用者の文書(`docs/user/cli-reference/05-snapshot.md`・`docs/user/snapshot-guide.md`)と確定仕様 - (`docs/specifications/snapshot-series.md` か `snapshot-exclude.md`)に書くこと、`CHANGELOG.md` の `[Unreleased]` - -含まない: - -- 新しい名前の世代の作成(`--name` を省いた `create`、`devbase up` の自動スナップショット)。この不具合は起きておらず、振る舞いを変えない -- 既存の世代へ差分を積む経路(`--full` なしの名前の指定)の判定の規則(対象ボリューム → 控えの起点 → 除外の一覧) -- 復元の対象・順序・失敗時の案内、`--point` の意味 -- 世代の保持・系列・ローテーションの規則、`created_at` の扱い(前提 3) -- すでに壊れた形で作り直された世代(`full.tar.zst` が差分で、前の `incr-*` が残る世代)を見つける・直す手段。 - 壊れた世代の中身は控えた時点から戻せないため、検出と案内を足すかは未決 1 に置く -- `snapshot copy`・`delete` の振る舞い -- `devbase-snapshot` のイメージの中身(tar・zstd の版)の変更 - -## ドメインイベント - -| # | イベント | 引き金 | 失敗したとき | 順序の前提 | -| --- | --- | --- | --- | --- | -| E1 | 利用者が既存の世代の名前で `snapshot create --full` を打った | 利用者の操作 | 名前が不正・場所がディレクトリでないときは今のとおり `SnapshotError` で止まり、世代を変えない | 世代 `backups/<名前>/` があり、`full.tar.zst` と 0 個以上の `incr-*.tar.zst`・`snapshot.snar` を持つ | -| E2 | 前の `snapshot.snar` を引き継がずに、ボリュームの全体を新しい full として控えた | E1 | tar・zstd が失敗したら E3・E4・E5 を行わず、世代のファイルを E1 の前と同じに保ち、終了コード 1 で止まる(受け入れ条件 5) | E1 | -| E3 | 前の差分アーカイブ(`incr-*.tar.zst`)と前の状態の控え(`snapshot.snar.bak`)を世代から除いた | E2 の成功 | 除けなかったら、その世代は復元で前の差分を当ててしまうため、成功として報告しない(受け入れ条件 6) | E2 の成功の後(前に除くと、E2 の失敗で前の控えを失う) | -| E4 | 世代の `meta.yml` を full の形(`files: [full.tar.zst]`・`incremental_count: 0`・今の除外の一覧)で書いた | E3 | 今のとおり例外で止まる | E3 | -| E5 | `snapshot.yml` のその世代の `incremental_count` と `updated_at` を更新した | E4 | 今のとおり例外で止まる | E4 | -| E6 | 利用者がその世代を復元し、ボリュームが E2 の時点の中身になった | 利用者の `snapshot restore` | 今の復元の失敗の案内のとおり | E5 | -| E7 | 次の `devbase up` の自動スナップショットが、その世代へ `incr-001` を積んだ | `devbase up` | 今の自動スナップショットの規則のとおり | E5(作り直した世代が系列の最新の世代で、積める条件を満たすとき) | - -## 用語 - -| 用語 | 意味 | -| --- | --- | -| 世代 | `backups/<名前>/` の 1 つ。`full.tar.zst` 1 つと、0 個以上の `incr-NNN.tar.zst` からなる(用語集の語) | -| 復元前バックアップ | `restore` が書き戻す前に自動で作るフルの世代 `pre-restore-<時刻>`(用語集の語) | -| 除外の一覧 | スナップショットのアーカイブに入れないパスの一覧(用語集の語) | - -## 受け入れ条件 - -- [ ] 1. 前提: 世代 `g` の `full.tar.zst` にファイル `a`・`b` があり、その後 `incr-001.tar.zst` が積まれている。ボリュームに `c` を足した - 操作: `create(name='g', full=True)` - 結果: `g/full.tar.zst` の中身の一覧(`tar -tf`)に `a`・`b`・`c` がすべて入る(実の GNU tar と zstd で確かめる) -- [ ] 2. 1 の後に `g` を復元すると、復元先のボリュームに `a`・`b`・`c` があり、`a`・`b` の中身は作り直した時点の中身と同じになる - (作り直す前の `incr-001` で変わった中身が上書きで戻らない) -- [ ] 3. 1 の後、`g` のディレクトリに `incr-*.tar.zst` と `snapshot.snar.bak` が無く、`g/meta.yml` は `type: full`・`files: [full.tar.zst]`・ - `incremental_count: 0` になり、`exclude` は作り直した時点の除外の一覧になる -- [ ] 4. 1 の後、`snapshot.yml` の `g` のエントリは 1 件のままで、`incremental_count` が 0 になり、`created_at` は作り直す前と同じ値である -- [ ] 5. 前提: 世代 `g` に `full.tar.zst`・`incr-001.tar.zst`・`snapshot.snar`・`meta.yml` がある - 操作: `create(name='g', full=True)` の控えのコマンドが失敗する - 結果: `SnapshotError` になり(CLI は終了コード 1)、`g` の 4 つのファイルは作り直す前とバイト列が同じで、ほかのファイルが増えていない。 - `snapshot.yml` も変わらない -- [ ] 6. 新しい full の控えには成功し、前の差分アーカイブを除くのに失敗したとき、`create` は成功として返らない(`SnapshotError`、CLI は終了コード 1) -- [ ] 7. 1 の後に `create(name='g')`(`--full` なし)を打つと `g/incr-001.tar.zst` ができ、その中身は 1 の後に変えたファイルだけである - (作り直した full から差分を積み直せる) -- [ ] 8. `snapshot.snar` の無い世代 `g`(`incr-*.tar.zst` を持つ)へ `create(name='g')` を打ち full へ切り替わったときも、3 と同じく - 前の `incr-*.tar.zst` が残らない -- [ ] 9. 退行しない: 新しい名前の世代の `create`(`--full` の有無とも)と `devbase up` の自動スナップショットのアーカイブ・`meta.yml`・ - `snapshot.yml` は今と同じになり、既存のテスト(`tests/snapshot/`・`tests/commands/`)がすべて通る -- [ ] 10. `docs/user/cli-reference/05-snapshot.md` と `docs/user/snapshot-guide.md` に「既にある世代の名前で `--full` を打つと、その世代を - 今のボリュームの全体で作り直し、前の差分は消える(前の時点へは戻せない)」ことが書かれている - -## 非機能の条件 - -| 大項目 | 条件 | -| --- | --- | -| 可用性 | 作り直しに失敗しても、その世代は作り直す前と同じに復元できる(受け入れ条件 5) | -| 性能・拡張性 | 作り直しの間は、前の世代のファイルと新しい `full.tar.zst` が同時に `backups/<名前>/` にあり得る。一時に要るディスクは最大で「前の世代の大きさ + 新しい full の大きさ」で、これを超える置き場を足さない | -| 運用・保守性 | 作り直したことが INFO の 1 行で分かる(世代の名前と、除いた差分の本数を含む) | - -## 影響 - -| 対象 | 影響 | -| --- | --- | -| 公開インタフェース | 変わらない(`snapshot create` の引数と終了コードは同じ。既存の世代への `--full` の結果だけが文書のとおりになる) | -| データ | 作り直した世代では前の `incr-*.tar.zst`・`snapshot.snar.bak` を消す。すでに壊れた形で作り直された世代は自動で直さない(未決 1) | -| 既存の振る舞い | 既存の世代の名前での `--full`(と、snar の無い世代への差分の切り替え)の結果。新しい世代・差分の積み増し・復元は変わらない | - -## 検証手段 - -| 項目 | 手段 | -| --- | --- | -| テスト | `env -u DEVBASE_ROOT uv run --locked pytest tests/ -q`。受け入れ条件 1・2・7 は実の GNU tar と zstd で控えのコマンドを走らせるテスト(`test_backup_real_tar.py` の形)で、ほかは docker を差し替えたテストで確かめる | -| 静的解析 | `uvx ruff check --select=E9,F63,F7,F82 lib` | -| 固有の語 | `python3 .github/scripts/proper_term_check.py`(「語の一覧が無いため飛ばした」と出たら検査していないと書く) | -| 手動確認 | リリース後テストで、作業用の名前の世代に対し `devbase snapshot create --name <作業用> --full` を 2 回(間にファイルを足して)打ち、`snapshot restore` を作業用のボリュームへ当てて全ファイルが戻ることを見る。利用者のボリュームへは復元しない。ボリュームを丸ごと写さない(AGENTS.md の落とし穴) | - -## 前提とする取り決め - -| 項目 | 参照先 / 決めたこと | -| --- | --- | -| プロジェクト構造 | 実装は `lib/devbase/snapshot/`、テストは `tests/snapshot/`。`containers/snapshot/` のイメージは変えない | -| コーディング規約 | `AGENTS.md` の「書き方」と周りのコード。lint は上の検証手段 | -| テスト戦略 | tar の中身に関わる条件(1・2・7)は実の GNU tar の結合のテスト、ファイルの残り・`meta.yml`・失敗の扱い(3〜6・8)は docker を差し替えた単体のテスト | - -## 境界 - -| 区分 | 内容 | -| --- | --- | -| 常に行う | 全体のテスト・lint・固有の語の検査、`CHANGELOG.md` の `[Unreleased]` への記入、利用者の文書と確定仕様の更新 | -| 確認してから行う | 承認ゲートで人が見る: 作り直した世代から前の差分を消すこと(前提 2)、未決 1 の扱い | -| 行わない | 利用者の実のボリュームでの復元、`devbase up` の実行(P3)、`devbase-snapshot` のイメージの道具や版の変更、すでにある利用者の世代の書き換え | - -## 未決 - -| 項目 | 誰が決めるか | 期限 | -| --- | --- | --- | -| 1. すでに壊れた形で作り直された世代(`meta.yml` の `files` が `[full.tar.zst]` なのに `incr-*.tar.zst` が残る世代)を、`list` や `restore` で見つけて警告するか。足さないなら、壊れた世代があり得ることを `CHANGELOG.md` に書くだけにする | 承認ゲート 1 で利用者(設計の案はこの課題の設計で示す) | 設計の承認まで | From df9d744665e20ae72a1bcc8c7de74c450a7dba8c Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Tue, 13 Oct 2026 00:55:03 +0900 Subject: [PATCH 2/3] =?UTF-8?q?Docs:=20secret-backend.md=20=E3=82=92?= =?UTF-8?q?=E7=A2=BA=E5=AE=9A=E4=BB=95=E6=A7=98=E3=81=AB=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/specifications/secret-backend.md | 144 +++++++-- issues/issue-295-design.md | 415 -------------------------- issues/issue-295-requirements.md | 244 --------------- 3 files changed, 122 insertions(+), 681 deletions(-) delete mode 100644 issues/issue-295-design.md delete mode 100644 issues/issue-295-requirements.md diff --git a/docs/specifications/secret-backend.md b/docs/specifications/secret-backend.md index 22e18019..ca16c063 100644 --- a/docs/specifications/secret-backend.md +++ b/docs/specifications/secret-backend.md @@ -25,7 +25,12 @@ OpenBao の置き場の並び(レイアウト)は `backend.yml` の版で選 backend が `openbao` の端末では、dev コンテナの中の OpenBao CLI(`bao`、base イメージに同梱)が 接続先 `BAO_ADDR` と `~/.vault-token` を受け取り、再起動せずに自分の機密を読み書きできる。 コンテナに置く資格情報は 1 時間で切れる token だけで、切れたらホストの `devbase env token` で -置き換える。 +置き換える。手元の loopback で待つサーバ(`http://127.0.0.1:8200` など)へも、コンテナから +届くように接続先を置き換えて渡す(「コンテナの中の `bao`」)。 + +OpenBao サーバは利用者が個人またはチームで立てる(セルフホスト)。devbasex は共通のサーバを +運用しない。devbase がサーバに求める条件は「サーバに求める条件」に、サーバの用意の手順と +ポリシーの雛形・試用の構成は [OpenBao サーバを用意する](../user/openbao-server.md) にある。 ### サーバ backend に OpenBao を採る理由 @@ -74,6 +79,9 @@ Infisical で個人単位の機密を守るには利用者ごとに project を | AWS の丸ごとの取り込み | `~/.aws/config` と `~/.aws/credentials` を切り出さずに 1 つの tar にして `AWS_CONFIG_BASE64` へ書くこと | | AWS のプロファイルの連なり | 選んだ AWS のプロファイルが動くのに要る節。`sso_session` が指す `[sso-session <名前>]` と、`source_profile` が指すプロファイル(たどれる限り) | +OpenBao サーバの持ち方に関わる語(利用の形・ポリシーの雛形・試用の構成・利用者の識別子・ +コンテナから見た接続先・管理する人)は [用語集](../glossary.md) の定義に従う。 + ## 構成要素 | 要素 | 置き場所 | 責務 | @@ -96,7 +104,9 @@ Infisical で個人単位の機密を守るには利用者ごとに project を | 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 を書き直す | +| `up` / `scale` の前処理と後処理 | `lib/devbase/commands/container.py` | backend を問わず、起動の先頭でグループの宣言を読み、ボリュームのグループ(プロセスの環境変数)と揃える(`_require_group_declaration`)。`_ensure_env_files` の子プロセスの `env init` へグループを渡す。backend が `openbao` のとき dev サービスへ `BAO_ADDR` を足し(`_bao_environment`)、コンテナから見た接続先へ置き換えるかを決める(`container_bao_address` / `_host_gateway_kwargs`)。起動後に起動の後の処理の段(`_run_post_start`)で後処理の対象へ token を書く。`devbase project post-start` も同じ段で token を書き直す | +| スケール構成の生成 | `lib/devbase/volume/compose.py` | `generate_scaled_compose` が受けた `host_gateway_environment` を dev のインスタンスにだけ当て、`host.docker.internal:host-gateway` の行を元の `extra_hosts` の形を保って足す(`_apply_host_gateway` / `_add_host_gateway`)。`network_mode: host` の dev サービスには当てない | +| ポリシーの雛形と試用の構成 | `docs/user/openbao/` | サーバの側で管理する人が使うファイル。`policies/` の HCL の雛形 5 つと、試用の構成 `compose.yml`。devbase は実行時に読まない | | base イメージ | `containers/base/Dockerfile` | OpenBao CLI `bao` を `checksums.txt` で検証して `/usr/local/bin` へ置く | | `env backend` コマンド | `lib/devbase/commands/env_backend.py` | `status` / `use` / `test` / `migrate` | | `env` コマンド | `lib/devbase/commands/env.py` | `--user` と `--group` の受け取り、`-p` とプロジェクトのグループの照合、`edit` の分岐、一覧の保存形式表示、`env token`。`env set` は `DEVBASE_ACCOUNT_GROUP` を置き場を開く前に拒む。`env sync` の同期の書き込み先(`SyncTargets`) | @@ -942,16 +952,52 @@ token をホストのディスクへ置かない決まりを守るためであ | `version: 2` | 権限はグループ単位で、ポリシーを付けていないグループの `team//` は 403 | | token の期限 | ログインで得る token は 1 時間で切れる(ロールの `token_ttl`)。devbase は期限の前に取り直す | -ポリシーの雛形は `docs/user/openbao/policies/` にある(個人 `devbase-user.hcl` は `version: 1` / `2` 共通、 -チームは `v1/` と `v2/` の `team-read.hcl` / `team-write.hcl`)。雛形は entity に付け、ロールの -`token_policies` は `default` だけにする。`version: 2` の雛形はグループの置き換え語 `__GROUP__` を -グループ名に置き換えて使う。雛形はマウント `devbase` の `data/` と `metadata/` の下だけを書き、 -`sys/`・`auth/`・`identity/` の経路を含まない。devbase は 1 回の操作で対象のグループのパスだけを -要求するため、グループ単位に絞ったサーバでも対象のグループの操作は通る。 +認証を AppRole だけにするのは、token の認証を足すと token の保存先と期限切れの扱いを持ち込むため +である。個人の形も `version: 2` を使い、プロジェクトはアカウントグループ `personal` を宣言する +(`group_aliases` は使わない)。形を 1 つにし、後からチームの形へ移りやすくするためである。 -試用の構成は `docs/user/openbao/compose.yml` で、開発モードの OpenBao をホストの `127.0.0.1:8200` に -だけ公開して立てる。OpenBao の版はこのファイルの `image` の 1 行だけに書き、base イメージの -`BAO_VERSION` と同じ 2.6 系に揃える。開発モードのサーバは機密を記憶にだけ置き、止めると消える。 +ポリシーの雛形は `docs/user/openbao/policies/` にある。 + +| ファイル | 書き込むポリシーの名前(文書の手順) | 許すこと | +| --- | --- | --- | +| `devbase-user.hcl`(`version: 1` / `2` 共通) | `devbase-user` | `devbase/data/users/{{identity.entity.name}}/*` の作成・読み取り・更新と、`devbase/metadata/users/{{identity.entity.name}}/*` の読み取り・一覧・削除 | +| `v1/team-read.hcl` | `devbase-team-read` | `team/global` と `team/projects/*` の読み取り | +| `v1/team-write.hcl` | `devbase-team-write` | 同じパスの作成・読み取り・更新 | +| `v2/team-read.hcl` | `devbase-team-read-` | `team//global` と `team//projects/*` の読み取り | +| `v2/team-write.hcl` | `devbase-team-write-` | 同じパスの作成・読み取り・更新 | + +- 雛形はマウント `devbase` の `data/` と `metadata/` の下だけを書き、`sys/`・`auth/`・`identity/` の + 経路を含まない。ほかのマウント名を使うときは、管理する人が `sed` で `devbase/` を置き換えてから書き込む +- `version: 2` の雛形はグループの置き換え語 `__GROUP__` を持ち、`sed 's/__GROUP__//g'` の 1 回で + そのグループだけの雛形になる。OpenBao のポリシーのテンプレートは entity の属するグループをパスへ + 列挙できず、entity の metadata にグループを持たせる形では 1 人が複数のグループに属せないためである +- 雛形は利用者の entity の `policies` に直に付け、ロールの `token_policies` は `default` だけにする。 + identity group は使わない。メンバーの一覧の書き直しで他の利用者の権限を巻き込まず、誰に何を許したかを + entity の一覧だけで読めるようにするためである。チーム共通への書き込みを許すのは、その利用者の entity に + 書き込みの雛形を足すことである +- 個人の雛形は本人の entity 名でパスを絞るため、entity 名と設定の `user` が違えば本人の置き場が 403 になる +- devbase は 1 回の操作で対象のグループのパスだけを要求するため、グループ単位に絞ったサーバでも + 対象のグループの操作は通る + +雛形と試用の構成を `docs/user/openbao/` に置くのは、devbase が実行時に読まず、`install.sh` の +`git clone` で利用者の `$DEVBASE_ROOT` に届き、文書から相対リンクで開けるためである(`etc/` は devbase が +実行時に使うファイルの置き場で、混ぜない)。雛形を書き出す副命令は持たない。副命令にできるのは +ファイルの写しだけで、公開インタフェースと「雛形の版と devbase の版の組み合わせ」を増やすためである。 + +試用の構成は `docs/user/openbao/compose.yml`(compose のプロジェクト名 `devbase-openbao-trial`、 +サービス名 `openbao`)で、公式イメージの開発モードの OpenBao をホストの `127.0.0.1:8200` にだけ公開して +立てる。OpenBao の版はこのファイルの `image` の 1 行だけに書き、base イメージの `BAO_VERSION` と同じ +2.6 系に揃える。root token を決めず、管理の操作はサーバのコンテナの中の `bao`(開発モードのサーバが +コンテナの中の `~/.vault-token` に置く root token)で打つ。ホストに `bao` を入れず、root token を画面に +出さずに試せるためである。開発モードのサーバは機密を記憶にだけ置き、止めると消える。 + +端末の失効はサーバの操作で、devbase のコマンドは持たない。文書の手順は次の形をとる。 + +- `secret_id` は発行のときに端末の名前を metadata(`terminal`)に入れ、失効は `secret-id` の一覧の + accessor と `secret-id-accessor/lookup` で端末を選んで `secret-id-accessor/destroy` で行う。管理する人が + `secret_id` の値を控えずに済み、漏れたときの範囲を広げないためである +- `secret_id` を消しても止まるのは新しいログインだけである。その端末へ発行済みの token は、token の + metadata に載る `role_name` と `terminal` で選び、`auth/token/revoke-accessor` で失効させる ### `SecretStore` の持ち回り @@ -1015,7 +1061,8 @@ backend が `openbao` のとき、`up` と `scale` は dev コンテナへ次を - 置き換えた値は `generate_scaled_compose` の `host_gateway_environment` で渡し、dev のインスタンスにだけ 当てる。元の dev サービスの `extra_hosts` はリストの形と辞書の形の両方を受けて元の形で足し、 - `host.docker.internal` の行が既にあれば足さない。`network_mode: host` の dev サービスには当てず、 + `host.docker.internal` の行が既にあれば足さない(リストの `name=ip` の形も同じ名前として見る)。 + リストでも辞書でもない形は触らず、行を足せなかった旨を警告する。`network_mode: host` の dev サービスには当てず、 `openbao.url` のままにする(loopback がそのまま届き、`extra_hosts` も使えない) - ホストの devbase が使う URL(`openbao.url`)・URL の検査・`backend.yml` の形は変わらない。 コンテナから見た接続先は `up` / `scale` のたびに組み直す @@ -1271,6 +1318,24 @@ flowchart TD - GCP の鍵ファイルの発見はログも標準出力も出さない。`env init` が登録する鍵ファイルと、`env sync`・控えの更新が プロファイル名から引く鍵ファイルは、同じ発見の規則(ファイル名の昇順・衝突は先勝ち・`default` は `~/google_credential.json` も引く)で決まる +- コンテナから見た接続先を置き換えるのは、`openbao.url` が `http` でホストが loopback、かつ docker の + 接続先がローカル扱いのときだけである。スキームとポートは保ち、それ以外(`https` を含む)は + `openbao.url` のまま渡す。破れれば `tests/commands/test_container_bao.py` が落ちる +- `host.docker.internal:host-gateway` の行は、置き換えたときだけ dev のインスタンスに足し、元の dev + サービスが `host.docker.internal` の行を持てば足さない。`network_mode: host` の dev サービスには + 置き換えも行も当てない。破れれば `tests/volume/test_compose_host_gateway.py` が落ちる +- コンテナから見た接続先を置き換えても、ホストの devbase が使う URL(`openbao.url`)・URL の検査・ + `backend.yml` の形は変わらない。破れれば `tests/env/test_backend_config.py`(URL の検査)と + `tests/commands/test_container_bao.py`(`BAO_ADDR` は `openbao.url` を元にする)が落ちる +- ポリシーの雛形はマウント `devbase` の `data/` と `metadata/` の下だけを書き、`sys/`・`auth/`・ + `identity/` の経路を含まない。個人の雛形は本人の entity 名の `users/<本人>/` の下だけ、チームの読み取りの + 雛形は読み取りだけを許し、`version: 2` の雛形は置き換えたグループの `team//` の下だけに効く。 + 破れれば `tests/docs/test_openbao_templates.py` が落ちる(サーバが実際に 403 を返すかは手動確認で見る) +- 試用の構成の OpenBao の版は `compose.yml` の `image` の 1 行だけにあり、base イメージの + `BAO_VERSION` と同じ 2.6 系である。ポートはホストの loopback にだけ公開し、root token を決めない。 + 破れれば `tests/docs/test_openbao_templates.py` が落ちる +- 雛形と文書に特定の組織の名前を書かない。破れれば固有の語の検査(`.github/scripts/proper_term_check.py`)が + 当たる。文書の例に実の token・`secret_id`・root token の値を書かないことは、テストを持たずレビューで見る ## データ・設定 @@ -1428,13 +1493,18 @@ groups: - devbase は root token を受け取らず、AppRole の `role_id` と端末ごとの `secret_id` だけで接続する。 試用の構成も root token を決めず、管理の操作はサーバのコンテナの中の `bao` で打つ。ポリシーの雛形は 本人以外の個人の置き場と `sys/` の管理の経路を許さない +- サーバの用意の文書は、例に実の token・`secret_id`・root token の値を書かず、`secret_id` をコマンド + 引数で渡す手順を示さない。接続する端末で発行できるとき(個人の形)は、発行のコマンドの出力を + `--secret-id-stdin` へパイプで渡し、画面にも出さない - コンテナから見た接続先の置き換えは `http` の loopback に限る。`https` の URL は置き換えず、TLS の 検証の相手を変えない - `scope` を SHA-256 にするのは、`role_id` とパスを平文で残さないためである - 個人単位の機密はパスで分け、他人のパスを読む権限が無ければサーバが 403 を返す。宛先を 指すのは設定、渡してよいかを決めるのはサーバ(entity 名で絞るポリシー)という分担である -- 端末ごとに `secret_id` を分けるため、1 台の失効が他の端末に及ばない。失効前に発行済みの - token は期限まで使えるため、期限はサーバ側で短く保つ +- 端末ごとに `secret_id` を分けるため、1 台の失効が他の端末に及ばない。`secret_id` の失効は + 新しいログインだけを止め、発行済みの token は期限まで使える。そのため期限はサーバ側で短く保ち + (1 時間)、端末を失効させるときはその端末の発行済みの token も accessor で失効させる + (「サーバに求める条件」) - `secrets/` は Git の除外対象で、`doctor` が実際に除外されることを確かめる - コンテナの `~/.vault-token` を読める者は、その token の期限(1 時間)まで本人として読み書き できる。長期の資格情報はコンテナへ置かない @@ -1466,6 +1536,12 @@ groups: 書き換えない。サーバの冗長化・バックアップ・監査ログ・TLS の証明書は OpenBao の文書に従う - 利用者は接続先 URL・`mount`・自分の識別子・`role_id`・端末ごとの `secret_id` で接続する。 個人の形では自分で作り、チームの形と既存のサーバの形ではサーバを管理する人から受け取る +- 試用の構成を試す前に `secrets/backend.yml` と `secrets/bootstrap.env.age` を退避し、片付けで戻す + (`devbase env backend use openbao` が 2 つを上書きし、既存の接続設定と資格情報を失うため)。退避が + 無ければ `devbase env backend use auto` で設定なしへ戻し、試用の `bootstrap.env.age` と `secrets/cache/` を + 消す。退避を戻すときは `secrets/cache/` を消さない(元の接続の控えを含み、試用の控えは scope が違うため + 元の接続では使われない)。どちらも最後に `devbase env backend status` が試用のサーバを指していないことを + 確かめる - 1 台の端末が同時に使う backend は 1 つ、扱う個人単位の機密は 1 人分である - 複数人の同時編集は版の不一致として後から書いた側で止まる。黙って上書きせず、読み直して やり直す。不達のときに書き込みを控えへ溜める経路は無い @@ -1684,14 +1760,38 @@ groups: `bao kv get` / `patch`、チーム共通への書き込みの 403、`env token` での取り直しも同じ。ポリシーによる 他人のパスと `users/` 直下の一覧の拒否、端末 1 台の `secret_id` の失効も同じ(開発モードの サーバでの確認結果は #166 と #295。本番のサーバでは配布後に確かめる) -- ポリシーの雛形(`version: 1` / `2`)を OpenBao 2.6 系の `bao policy write` で書き込めること、2 人の - 利用者で他人の個人の置き場の 403・権限の無い者のチーム共通への書き込みの 403 になること、試用の構成で - 接続から `devbase up` とコンテナの中の `bao kv get` まで通ることは手動確認(#295) -- コンテナから見た接続先の表の各行、`extra_hosts` のリストと辞書の形、既にある - `host.docker.internal` の行、`network_mode: host` は pytest(`tests/commands/test_container_bao.py`・ - `tests/volume/test_compose_host_gateway.py`) -- 雛形と試用の構成の形(パスの接頭辞・管理の経路を含まない・版が `BAO_VERSION` と同じ 2.6 系・ポートが - loopback だけ)は pytest(`tests/docs/test_openbao_templates.py`) +- コンテナから見た接続先が、表の各行(`http` の `localhost` / `127.0.0.1` / `[::1]`、ポートの有無、 + リモート扱い、`https` の loopback と loopback 以外)のとおりになり、置き換えないときは構成の生成へ何も + 渡さないこと(`tests/commands/test_container_bao.py`) +- 置き換えた値が dev のインスタンスにだけ当たり、`extra_hosts` のリストと辞書の形を保って 1 行足し、既に + ある `host.docker.internal` の行を重ねず、解釈できない形の `extra_hosts` は触らず、`network_mode: host` の + dev サービスには置き換えも行も当てないこと。置き換えないときの生成の結果が今までと同じであること(`tests/volume/test_compose_host_gateway.py`) +- 5 つのポリシーの雛形があり、どの `path` もマウント `devbase` の `data/` か `metadata/` の下で管理の経路と + 管理の操作を含まないこと、個人の雛形が本人の entity 名のパスだけ、読み取りの雛形が読み取りだけ、 + `version: 2` の雛形が `__GROUP__` の置き換えで 1 つのグループの `team//` だけを指すこと。試用の + 構成の版が `BAO_VERSION` と同じ 2.6 系で、ポートが loopback だけで、root token を決めないこと + (`tests/docs/test_openbao_templates.py`) +- 次は OpenBao 2.6 系の開発モードのサーバと作業用のプロジェクトでの手動確認(#295。確かめた環境は + macOS の Docker Desktop) + - 5 つの雛形が `bao policy write` で拒まれないこと(`version: 2` は `__GROUP__` を置き換えて) + - `version: 2` の雛形で、チーム共通への書き込みを 1 人(`member01`)にだけ許したとき、他人の個人の + 置き場の読み取り・`users/` 直下の一覧・`member02` のチーム共通への書き込み・ポリシーを付けていない + グループの `team//`・`sys/` の経路が 403 になり、本人の置き場の読み書き・`member01` の + チーム共通への書き込み・両者のチーム共通の読み取りが通ること。`version: 1` の雛形でも、他人の個人の + 置き場の 403 とチーム共通への書き込みの制限が同じく効くこと + - 1 人の端末 1 台の `secret_id` を accessor で失効させると、その端末の `devbase env backend test` が + ログインの失敗で止まり、同じ利用者の別の端末では通ること + - 試用の構成で、文書と雛形だけを見て接続し、`DEVBASE_ACCOUNT_GROUP=personal` を宣言したプロジェクトで + `env set` → `env get` が書いた値を返し、`devbase up` のコンテナの環境変数にその値があり、コンテナの + 中の `bao kv get` が本人の個人共通の置き場を読めること + - 試用の構成の片付けの後に `devbase env backend status` が試用のサーバを指していないこと +- 既存のサーバの形の利用者が、`backend.yml` と `bootstrap.env.age` を変えずに、これまでと同じコマンドで + 機密を読み書きできること。`https` の `openbao.url` で生成の結果が変わらないことは pytest(上の 2 つ)で + 見て、実のサーバへの接続は配布後の手動確認で見る +- Linux の Docker で、ホストの `127.0.0.1` にだけ公開したポートへコンテナから `host-gateway` で届くかは + 確かめていない(「コンテナの中の `bao`」) +- `docs/user/` と確定仕様に、特定の組織のサーバや運用の決まりを前提にした記述(利用者の識別子を組織の + メールアドレスとする説明・非公開のリポジトリへの参照)が無いことはレビューと固有の語の検査で見る ## 関連リンク diff --git a/issues/issue-295-design.md b/issues/issue-295-design.md deleted file mode 100644 index 4da1d37a..00000000 --- a/issues/issue-295-design.md +++ /dev/null @@ -1,415 +0,0 @@ -# OpenBao backend: 管理者のいない個人やチームは、サーバの立て方も devbase が前提にする権限も分からず使えない → 公開の文書とポリシーの雛形だけで自分のサーバを立てて接続でき、手元のサーバでもコンテナの中の `bao` が届く(#295) - -## 目的 - -- **何が壊れているか**: 利用者向けの文書と確定仕様が、ある組織の運用するサーバと非公開の運用の決まりを前提にしている。サーバの用意の手順も、devbase が前提にするポリシーも公開されていない。手元のサーバ(`http://127.0.0.1:8200`)では、コンテナへ渡す `BAO_ADDR` がコンテナ自身を指し、コンテナの中の `bao` が届かない -- **誰が困るか**: 管理者のいない個人と小さなチーム。既存のサーバへ接続している利用者は困っていない -- **直すと何が成り立つか**: 公開の文書と雛形だけで、試用の構成か自分のサーバを立て、`devbase env backend use openbao` から `devbase up` の機密の読み込みまで通せる。手元のサーバでも、dev コンテナの中の `bao kv get` が本人の置き場を読める - -## 適用範囲 - -- **働く範囲**: このリポジトリの利用者向けの文書・確定仕様・ポリシーの雛形・試用の構成と、`devbase up` / `devbase scale` が組む dev コンテナの構成(`BAO_ADDR` と `extra_hosts`)。プラグインのリポジトリは変えない -- **プロジェクトごとに違うもの**: 無し(接続先は端末ごとの `secrets/backend.yml`、グループは今どおりプロジェクトの `env` の宣言で受ける) -- **当たるモード**: `standard` - -## あるべき姿の根拠 - -| 根拠 | 種類 | 何を示すか | -| --- | --- | --- | -| 「OSS として多くの人が利用する場合は、利用者が個人・チームでそれぞれのサーバを立てる前提での運用が必要」(2026-09-26) | 利用者の指示の原文 | 文書と雛形をセルフホストの前提に揃える | -| 「個人の形も `version: 2` にそろえ、宣言するアカウントグループの名前は `personal` にする」「認証は AppRole だけにする」(2026-10-11) | 利用者の指示の原文 | 個人の形の既定と、token の認証を足さないこと | -| OpenBao 2.6.2 の開発モードのサーバで、決定 6・7 のポリシーを書き込み、`member01`・`member02` の 2 人で読み書きを打った(2026-10-12、macOS の Docker Desktop の端末) | 実測 | 他人の個人の置き場の読み取りが 403(`version: 1` / `2` とも)、権限の無い者のチーム共通への書き込みが 403、`version: 2` で別のグループの `team//` の読み取りが 403、`users/` 直下の一覧が 403、本人の置き場の未作成のパスが 404、`cas: 0` の書き込みが 200。`secret_id` を accessor で失効させると、その `secret_id` のログインが 400、同じ利用者の別の `secret_id` は通る | -| 同じ端末で、ホストの loopback(`127.0.0.1:18200`)で待つサーバへ、base イメージ(`devbase-base:latest`)のコンテナから `bao status` を打った | 実測 | `BAO_ADDR=http://127.0.0.1:18200` は届かない。`http://host.docker.internal:18200` は、`--add-host host.docker.internal:host-gateway` の有無によらず届く | -| 同じ端末で、公式イメージ `openbao/openbao:2.6.2` を compose の 1 ファイルで開発モードに起動した | 実測 | `docker compose exec openbao bao …` が、root token を画面に出さずに管理の操作を通す(開発モードのサーバがコンテナの中の `~/.vault-token` へ root token を書く)。ホストへ `127.0.0.1` だけで公開したポートへ、別のコンテナから `host.docker.internal` で届く | -| [OpenBao: AppRole auth](https://openbao.org/docs/auth/approle/) / [Identity](https://openbao.org/docs/concepts/identity/) / [Policies](https://openbao.org/docs/concepts/policies/) | 外部の一次情報 | AppRole の entity alias の名前は `role_id` である。ポリシーの `{{identity.entity.name}}` で本人のパスを絞れる | - -要求と受け入れ条件は #295 の本文にある(コピーは `issues/issue-295-requirements.md`)。この文書は「どう作るか」だけを扱う。 - -## ドメインモデル - -### コンテキスト - -| コンテキスト | 何の語が 1 つの意味に決まるか | -| --- | --- | -| 機密の置き場(`secret`) | 利用の形・ポリシーの雛形・試用の構成・利用者の識別子・コンテナから見た接続先 | -| docker の接続先(`docker-context`) | ローカル扱い・リモート扱い | - -機密の置き場が docker の接続先に順応する(順応者)。コンテナから見た接続先は、docker の接続先が決めた「ローカル扱いかどうか」をそのまま受け取り、自分では判定しない。 - -### 集約 - -| 集約 | 持ち主(書き換えてよいもの) | 根 | エンティティ | 値オブジェクト | -| --- | --- | --- | --- | --- | -| ポリシーの雛形 | `docs/user/openbao/policies/` のファイル | 雛形の組 | 個人の雛形・チームの読み取りの雛形・チームの書き込みの雛形(`version: 1` / `2`) | パスの規則・許す操作(capabilities)・グループの置き換え語 `__GROUP__` | -| 試用の構成 | `docs/user/openbao/compose.yml` | 試用のサーバ | — | OpenBao の版・公開するポート | -| dev コンテナの接続先 | dev 環境の組み立て(`commands/container.py` の `_bao_environment`) | dev サービスへ足す設定 | — | コンテナから見た接続先・`extra_hosts` の 1 行 | - -サーバの上の権限の構成(マウント・AppRole・entity・alias・ロール・`secret_id`)は利用者のサーバの持ち物で、devbase は持たない。文書は管理する人が打つ手順を示すだけで、devbase のコードはサーバの構成を書き換えない(前提 5)。 - -### 不変条件 - -| # | 集約 | 条件 | 破れたときの扱い | -| --- | --- | --- | --- | -| I1 | ポリシーの雛形 | 個人の雛形は、本人(`{{identity.entity.name}}`)の `users/<本人>/` の下だけを読み書きさせる。他人の `users/<名前>/` と `users/` 直下の一覧は許さない | 他人の置き場の読み取りが通る → 実機の確認(受け入れ条件 14)で落ちる | -| I2 | ポリシーの雛形 | チームの読み取りの雛形は読み取りだけを許す。チーム共通へ書けるのは、書き込みの雛形を付けた者だけ | 書き込みの雛形の無い者の書き込みが通る → 受け入れ条件 14 で落ちる | -| I3 | ポリシーの雛形 | `version: 2` の雛形は、`__GROUP__` を置き換えたグループの `team//` の下だけに効く | 別のグループの `team/` が読める → 受け入れ条件 14 で落ちる | -| I4 | ポリシーの雛形 | どの雛形も `sys/` と `auth/` と `identity/` の経路を含まず、KV v2 のマウント `devbase` の `data/` と `metadata/` の下だけを書く。特定の組織の名前を含まない | 雛形の検査(テスト設計)で落ちる | -| I5 | 試用の構成 | OpenBao の版は `compose.yml` の `image` の 1 行だけにあり、base イメージの `BAO_VERSION` と同じ 2.6 系である | 雛形の検査で落ちる | -| I6 | 試用の構成 | サーバのポートはホストの loopback(`127.0.0.1`)にだけ公開する | 雛形の検査で落ちる | -| I7 | dev コンテナの接続先 | `openbao.url` のスキームが `http` でホストが loopback で、docker の接続先がローカル扱いのときだけ、コンテナから見た接続先のホストを `host.docker.internal` に置き換える。スキーム・ポートは保つ。それ以外(`https` を含む)は `openbao.url` のまま | pytest で落ちる | -| I8 | dev コンテナの接続先 | 置き換えたときだけ、dev のインスタンスへ `host.docker.internal:host-gateway` の 1 行を足す。元の dev サービスが `host.docker.internal` の行を持っていれば足さない | pytest で落ちる | -| I9 | dev コンテナの接続先 | ホストの devbase が使う URL(`openbao.url`)・URL の検査・`secrets/backend.yml` の形は変わらない | pytest で落ちる(既存のテストが変わらず通る) | -| I10 | 試用の構成・文書 | 文書の例に実の token・`secret_id`・root token の値を書かず、`secret_id` をコマンド引数で渡す手順と、devbase の接続に root token を使う手順を書かない | 文書の検査(テスト設計)で落ちる | - -### ドメインイベント - -| # | イベント | 発生元 | 受け手 | -| --- | --- | --- | --- | -| E1 | 利用者が利用の形を選んだ | 利用者(サーバの用意の文書の冒頭の表) | 選んだ形の節 | -| E2 | 利用者が OpenBao サーバを起動した | 試用の構成の `docker compose up` か、利用者の自分のサーバ | E3 の手順 | -| E3 | 管理する人が KV v2 を既定の `devbase` にマウントした | `bao secrets enable` | E4 の手順 | -| E4 | 管理する人が AppRole を有効にし、雛形のポリシーを書き込んだ | `bao auth enable` / `bao policy write` | E5 の手順 | -| E5 | 管理する人が利用者の entity・alias・ロールを作った | `bao write identity/entity` ほか | E6 の手順 | -| E6 | 管理する人が端末の `secret_id` を発行した | `bao write … /secret-id` | E7(`--secret-id-stdin` へ直に渡す) | -| E7 | 利用者が `devbase env backend use openbao` で接続した | 利用者 | `secrets/backend.yml` / `secrets/bootstrap.env.age` | -| E8 | 利用者がプロジェクトで `DEVBASE_ACCOUNT_GROUP=personal` を宣言した | 利用者 | 機密のコマンドと `up` のグループの決定(今の振る舞い) | -| E9 | 利用者が `env set` / `env get` で機密を読み書きした | 利用者 | サーバ(ポリシーが通すか拒む) | -| E10 | `devbase up` がコンテナへ機密を入れた | `devbase up` | dev コンテナの環境変数と、コンテナから見た接続先(I7・I8) | -| E11 | コンテナの中の `bao` がサーバへ届いた | コンテナの中の利用者 | サーバ | -| E12 | 管理する人が端末の `secret_id` を失効させた | `bao write … /secret-id-accessor/destroy` | その端末の次のログイン(400 で止まる) | -| E13 | 利用者が試用の構成を片付けた | 利用者 | 試用のサーバ(消える)と、端末の backend の設定(元へ戻る) | - -### 用語 - -| 用語 | 意味 | 用語集への反映 | -| --- | --- | --- | -| 利用の形 | OpenBao サーバの持ち方の 3 つの区分。個人の形・チームの形・既存のサーバの形 | 意味の変更(出所を確定仕様へ) | -| ポリシーの雛形 | devbase が前提にする権限を OpenBao のポリシー(HCL)として書いた、`docs/user/openbao/policies/` のファイル | 意味の変更(置き場と出所を確定する) | -| 試用の構成 | `docs/user/openbao/compose.yml` で開発モードの OpenBao を手元に立て、接続を試して片付けるまでの最小の構成 | 意味の変更(置き場と出所を確定する) | -| 利用者の識別子 | `secrets/backend.yml` の `openbao.user`。サーバの entity 名と同じ値にし、個人単位の置き場のパスに使う | 追加 | -| コンテナから見た接続先 | dev コンテナへ `BAO_ADDR` として渡す接続先。`openbao.url` が `http` の loopback でローカル扱いのときはホストを `host.docker.internal` に置き換えた値、それ以外(`https` を含む)は `openbao.url` のまま | 追加 | -| 管理する人 | 利用者のサーバで、マウント・AppRole・ポリシー・entity を作る人。個人の形では利用者本人 | 追加 | - -## 機能一覧 - -| # | 機能 | 誰が使うか | -| --- | --- | --- | -| F1 | 利用の形の 3 つの違いと、形ごとに用意するものを読み、形を選ぶ | 利用者 | -| F2 | 試用の構成でサーバを手元に立て、接続を試し、片付ける | 個人の利用者 | -| F3 | サーバへ KV v2・AppRole・ポリシー・entity・alias・ロールを用意し、端末の `secret_id` を発行して接続する | 管理する人(個人の形では本人) | -| F4 | 利用者と端末を足し、チーム共通への書き込みを許し、端末 1 台の `secret_id` を失効させる | チームの管理する人 | -| F5 | ポリシーの雛形(`version: 1` / `2`)をサーバへ書き込む | 管理する人 | -| F6 | 既存のサーバへ、管理者から受け取った値で接続する | 既存のサーバの利用者 | -| F7 | 手元のサーバ(loopback の `openbao.url`)でも、dev コンテナの中の `bao` がサーバへ届く | 個人の利用者 | -| F8 | devbase がサーバに求める条件を確定仕様で読む | 管理する人・開発者 | - -## 構成要素 - -| 要素 | 責務 | 機能 | -| --- | --- | --- | -| サーバの用意の文書(`docs/user/openbao-server.md`、新設) | 利用の形の 3 つを見出しで分け、形ごとの用意・試用の構成・管理の手順・片付けを示す | F1〜F6 | -| 機密の保存先の文書(`docs/user/env-backend.md`) | 接続する側の手順だけを持つ。「OpenBao 側で用意するもの」をサーバの用意の文書への案内と受け取る値の表に変え、管理者から受け取るのは既存のサーバの形だけにする。コンテナから見た接続先を書く | F1・F6・F7 | -| ポリシーの雛形(`docs/user/openbao/policies/`、新設) | 個人・チームの読み取り・チームの書き込みの権限を HCL で持つ | F5 | -| 試用の構成(`docs/user/openbao/compose.yml`、新設) | 開発モードの OpenBao を、版を 1 か所に書いて loopback にだけ公開して立てる | F2 | -| dev 環境の組み立て(`commands/container.py` の `_bao_environment`) | コンテナから見た接続先を決め、置き換えたかを返す | F7 | -| スケール構成の生成(`volume/compose.py`) | 受け取った `extra_hosts` の行を、元の行と重ねずに dev のインスタンスへ足す | F7 | -| 確定仕様(`docs/specifications/secret-backend.md`) | サーバに求める条件・雛形の置き場・利用者の識別子・コンテナから見た接続先を持つ。非公開のリポジトリへの参照を外す | F8 | -| 文書の目次(`docs/README.md`)と `CHANGELOG.md` | 新しい文書への入口と、利用者に見える変更の記入 | F1 | - -文脈(外部との関係): - -```mermaid -graph LR - 利用者 --> 文書[devbase の文書と雛形] - 管理する人 --> 文書 - 管理する人 -->|bao の管理の操作| サーバ[利用者の OpenBao サーバ] - 利用者 -->|devbase env / up| CLI[devbase] - CLI -->|AppRole のログインと KV v2| サーバ - CLI -->|compose の構成| Docker[Docker Engine / Desktop] -``` - -OpenBao サーバと Docker は devbase が変えられない外部である。文書・雛形・試用の構成・`devbase` は同じ変更で変える内部である。 - -構成要素: - -```mermaid -graph TD - subgraph 文書と雛形 - G[サーバの用意の文書] - B[機密の保存先の文書] - P[ポリシーの雛形] - T[試用の構成] - S[確定仕様] - end - subgraph devbase の up と scale - E[dev 環境の組み立て] - C[スケール構成の生成] - end - G --> P - G --> T - G --> B - B --> G - S --> P - E -->|コンテナから見た接続先と extra_hosts| C -``` - -配置(手元のサーバ、ローカル扱い): - -```mermaid -graph TD - subgraph 利用者の端末 - CLI[devbase] - subgraph Docker - BAO[試用のサーバ 0.0.0.0:8200] - DEV[dev コンテナ] - end - end - CLI -->|http://127.0.0.1:8200 公開したポート| BAO - DEV -->|http://host.docker.internal:8200| BAO - CLI -->|docker exec で token 1 行| DEV -``` - -境界をまたぐのは、ホストからサーバへの AppRole のログインと KV v2 の要求(`role_id` / `secret_id` / token)、ホストからコンテナへの 1 時間の token(今と同じ `~/.vault-token`)、コンテナからサーバへの token 付きの要求である。`secret_id` はホストから出ない(今と同じ)。 - -置き場所(新設は `+`、変更は `~`): - -```text -docs/ -├── README.md ~ 新しい文書を目次に足す -├── specifications/secret-backend.md ~ サーバに求める条件・コンテナから見た接続先 -└── user/ - ├── env-backend.md ~ 接続する側だけにする - ├── openbao-server.md + サーバの用意(利用の形の 3 つ) - └── openbao/ - ├── compose.yml + 試用の構成 - └── policies/ - ├── devbase-user.hcl + 個人の雛形(version 1 / 2 共通) - ├── v1/team-read.hcl + - ├── v1/team-write.hcl + - ├── v2/team-read.hcl + __GROUP__ を置き換えて使う - └── v2/team-write.hcl + -lib/devbase/ -├── commands/container.py ~ _bao_environment -└── volume/compose.py ~ extra_hosts を dev のインスタンスへ足す -tests/ -├── commands/test_container_bao.py ~ -├── volume/ ~ 生成のテスト(既存のファイルへ足す) -└── docs/test_openbao_templates.py + 雛形と試用の構成の検査 -CHANGELOG.md ~ [Unreleased] -``` - -## 構造 - -型は足さず、変えない。`_bao_environment` は `BAO_ADDR` を返す今の役目に、置き換えたときの `extra_hosts` の 1 行を返す役目を足し、スケール構成の生成がその行を dev のインスタンスへ足す(入出力の契約の表)。返し方の形は実装で決める。読む型は次の 2 つだけである。 - -```mermaid -classDiagram - class OpenBaoSettings { - url: str - } - class DockerTarget { - remote: bool - } - class _bao_environment { - <> - } - _bao_environment ..> OpenBaoSettings : url を読む - _bao_environment ..> DockerTarget : remote を読む -``` - -## 入出力の契約 - -コンテナへ渡す設定(`devbase up` / `devbase scale` の dev のインスタンス): - -| `openbao.url` | docker の接続先 | `BAO_ADDR` | dev のインスタンスへ足す `extra_hosts` | -| --- | --- | --- | --- | -| `http://127.0.0.1:8200` | ローカル扱い | `http://host.docker.internal:8200` | `host.docker.internal:host-gateway` | -| `http://localhost:8200` / `http://[::1]:8200` | ローカル扱い | `http://host.docker.internal:8200` | 同上 | -| `http://localhost`(ポートなし) | ローカル扱い | `http://host.docker.internal` | 同上 | -| loopback のどれか | リモート扱い | `openbao.url` のまま | 足さない | -| `https://localhost:8200` など `https` の loopback | どちらでも | `openbao.url` のまま | 足さない | -| `https://openbao.example.com` | どちらでも | `openbao.url` のまま | 足さない | -| backend が `openbao` でない | — | 渡さない(今と同じ) | 足さない | - -- 元の dev サービスの `extra_hosts` は、リストの形(`"name:ip"`)と辞書の形(`name: ip`)の両方を受け、元の形を保って足す。`host.docker.internal` の行が既にあれば、どちらの形でも足さない(I8) -- コンテナから見た接続先は `up` のたびに組み直す。今動いているコンテナは、次の `up` で新しい `BAO_ADDR` になる -- `devbase env token` と `project post-start` が書く `~/.vault-token` は変えない - -ポリシーの雛形と試用の構成の約束(文書が指すもの): - -| もの | 約束 | -| --- | --- | -| 雛形のファイル名 | 上の置き場所のツリーの 5 つ。書き込むポリシーの名前は文書が決める(個人 `devbase-user`、`version: 1` は `devbase-team-read` / `devbase-team-write`、`version: 2` は `devbase-team-read-` / `devbase-team-write-`) | -| 置き換え語 | `version: 2` の雛形の `__GROUP__` だけ。`sed 's/__GROUP__/personal/g'` の 1 回で使える形にする | -| マウント名 | 雛形は `devbase` を書く。ほかのマウント名を使う管理する人は、同じく `sed` で置き換える(文書に書く) | -| 試用の構成の名前 | compose のプロジェクト名 `devbase-openbao-trial`、サービス名 `openbao`、ホストのポート `127.0.0.1:8200` | - -## 処理の流れ - -試用の構成で個人の形を通す流れ(サーバの用意の文書の手順の順序): - -```mermaid -sequenceDiagram - actor U as 利用者 - participant T as 試用のサーバ - participant D as devbase - participant C as dev コンテナ - U->>T: docker compose up -d と bao status - U->>T: secrets enable と auth enable approle - U->>T: policy write(個人と personal のチームの書き込み) - U->>T: ロールと entity と alias(名前は role_id) - U->>D: 既存の backend.yml と bootstrap.env.age を退避 - U->>T: secret_id を発行 - T-->>D: 標準入力で backend use openbao --secret-id-stdin - U->>D: プロジェクトの env に DEVBASE_ACCOUNT_GROUP=personal - U->>D: env set と env get - D->>T: AppRole のログインと KV v2 - U->>D: devbase up - D->>C: 機密と BAO_ADDR(host.docker.internal)と token - U->>C: bao kv get users/<本人>/personal/global - C->>T: token 付きの要求 - U->>T: docker compose down - U->>D: 退避した 2 つを戻すか backend use auto -``` - -`devbase up` / `scale` でコンテナから見た接続先を決める流れ: - -```mermaid -graph TD - subgraph dev 環境の組み立て - A[backend が openbao か] -->|いいえ| N[何も足さない] - A -->|はい| B[openbao.url が http でホストが loopback か] - B -->|いいえ| K[BAO_ADDR = openbao.url] - B -->|はい| R[docker の接続先がリモート扱いか] - R -->|はい| K - R -->|いいえ| H[BAO_ADDR のホストを host.docker.internal に置き換え] - end - subgraph スケール構成の生成 - X[元の dev サービスに host.docker.internal の行があるか] - X -->|ある| G[extra_hosts は足さない] - X -->|無い| Y[dev のインスタンスへ host-gateway の 1 行を足す] - end - H --> X -``` - -1 枚目は、サーバの用意の文書が並べる手順を、試用の構成・ポリシーの雛形・機密の保存先の文書(接続の手順)の順にたどる。確定仕様・文書の目次・`CHANGELOG.md` は処理に現れないため、どちらの図にも含めない。 - -## 非機能の実現方式 - -| 大項目 | 要求の条件 | 実現方式 | 確かめ方 | -| --- | --- | --- | --- | -| セキュリティ | ポリシーの雛形は、受け入れ条件 12 に並べた権限のほかを許さない(本人以外の個人の置き場・`sys/` の管理の経路を利用者のポリシーに含めない) | 雛形は `devbase/data/` と `devbase/metadata/` の下だけを書く。個人の雛形は `users/{{identity.entity.name}}/` の下に限り、チームの読み取りの雛形は `read` だけにする。利用者のロールの `token_policies` は `default` だけにし、雛形は entity へ付ける | 雛形の検査(パスの接頭辞と capabilities)と、実機の 2 人の読み書き(受け入れ条件 14) | -| セキュリティ | 文書の例に実の token・`secret_id`・root token の値を書かない。devbase の接続に root token を使う手順を示さない。`secret_id` をコマンド引数で渡す手順を示さない | 試用の構成は root token を決めず、管理の操作はサーバのコンテナの中の `bao`(開発モードが書いた `~/.vault-token`)で打つ。`secret_id` は発行のコマンドの出力を `devbase env backend use … --secret-id-stdin` へパイプで渡し、画面にも引数にも出さない | 文書の検査(`--secret-id ` の後に値を置く形・`root` の token の値・`BAO_TOKEN=` の値の無いこと)と、レビュー | -| 運用・保守性 | 雛形と試用の構成は OpenBao の版を 1 か所に書き、base イメージの `BAO_VERSION` と同じ 2.6 系に揃える | 版は `compose.yml` の `image: openbao/openbao:<版>` の 1 行だけに書き、文書は版を写さずその行を指す。雛形は版を持たない | 雛形の検査が、`image` の版と `containers/base/Dockerfile` の `ARG BAO_VERSION` の 2 つ目までの数字を比べる | -| システム環境 | 試用の構成は macOS の Docker Desktop と Linux の Docker の両方で動く手順を示す。片方でしか確かめていなければ、文書に確かめた環境を書く | 試用の構成は Docker と compose だけを使い、ホストに `bao` を求めない。コンテナの中の `bao` の到達は、Linux の Docker では、ホストの loopback にだけ公開したポートへ `host-gateway` から届かない見込みのため、文書に「確かめた環境」と「Linux の Docker での扱い」を分けて書く(未確認のまま残ること) | 実装で Linux の Docker の端末を使えれば試用の構成とコンテナの中の `bao` を打つ。使えなければ文書に確かめた環境(macOS の Docker Desktop)を書く | - -## 決定の記録 - -### 決定 1: 雛形を文書から相対リンクで開けるようにするため、ポリシーの雛形と試用の構成を `docs/user/openbao/` に置く - -雛形と試用の構成はサーバの側で使うもので、devbase の実行時には読まない。`install.sh` は `git clone` でリポジトリ全体を配るため、`docs/` に置いても利用者の `$DEVBASE_ROOT` に届き、手順は `$DEVBASE_ROOT/docs/user/openbao/…` を指せる。GitHub の上でも文書から相対リンクで開ける。 - -`etc/` は devbase が実行時に使う補完のファイルの置き場で、サーバへ書き込むファイルを混ぜると「devbase が読むもの」の境界が崩れるため採らない。 - -根拠: Mission / Value 1(MVV 版 1) - -### 決定 2: 接続する側とサーバを用意する側の読み手を分けるため、サーバの用意を新しい文書 1 本にまとめ、利用の形の 3 つを見出しで分ける - -`docs/user/openbao-server.md` を新しく置き、冒頭に利用の形の 3 つ(個人・チーム・既存のサーバ)の違いと用意するものの表、続けて形ごとの見出しを置く。個人とチームの手順は同じ管理のコマンドを共有し、チームの節は利用者と端末を足す・書き込みを許す・失効させる手順だけを足す。`env-backend.md`(619 行)は接続する側の手順に絞り、「OpenBao 側で用意するもの」はこの文書への案内と受け取る値の表にする。 - -形ごとに 3 本へ分けると、個人とチームで同じ管理のコマンドを 2 か所に写すことになる。`env-backend.md` へ足すと、接続する利用者がサーバの構築の手順を読み飛ばす分量になる。 - -根拠: Mission(MVV 版 1) - -### 決定 3: 公開インタフェースを増やさないため、雛形を書き出す副命令を足さず、文書と雛形のファイルだけにする - -雛形は `$DEVBASE_ROOT/docs/user/openbao/` に既にあり、管理する人は `bao policy write <名前> - < <ファイル>` で書き込める。副命令が代わりにできるのはファイルの写しだけで、サーバの構成は管理する人の `bao` が書く(前提 5)。 - -副命令を足すと、既存のサーバの形の利用者にも見えるコマンドが増え、雛形の版と devbase の版の組み合わせを持つことになる。 - -根拠: Mission / Value 1(MVV 版 1) - -### 決定 4: 手元のサーバでもコンテナの中の `bao` を使えるようにするため、`http` の loopback の `openbao.url` はローカル扱いのときだけ、コンテナへ渡す `BAO_ADDR` のホストを `host.docker.internal` に置き換え、`host-gateway` の行を足す - -コンテナの中の `127.0.0.1` はコンテナ自身を指し、loopback の `openbao.url` をそのまま渡すと届かない。Docker Desktop で `host.docker.internal` がホストの loopback のサーバへ届くことを実測した(あるべき姿の根拠)。`host-gateway` の行は Linux の Docker で `host.docker.internal` を引けるようにするためで、Docker Desktop でも行の有無によらず届いた。リモート扱いでは loopback がホストの端末を指し、コンテナのある端末から届かないため置き換えない。`https` の loopback は置き換えない。証明書の名前が `host.docker.internal` と合わず、TLS の検証で落ちるためである(受け入れ条件 16)。ホストの devbase の URL と検査は変えない(I9)。 - -文書で「コンテナの中では `export BAO_ADDR=…` を打つ」と案内する形は、シェルを開くたびに打つことになり、`devbase up` だけで使える今の形と揃わない。「使えない」と書く形は、試用の構成で dev コンテナの `bao` を確かめられない。 - -根拠: Mission / Vision(MVV 版 1) - -### 決定 5: ホストに `bao` を入れずに試せるようにするため、試用の構成を compose の 1 ファイルにし、管理の操作をサーバのコンテナの中の `bao` で打つ - -`openbao/openbao:<版>` を開発モードで起動し、ポートを `127.0.0.1:8200` にだけ公開する。管理の操作は `docker compose … exec -T openbao bao …` を包むシェル関数(文書では `bao_admin`)で打つ。開発モードのサーバがコンテナの中に root token を置くため、root token を画面にも文書にも出さずに済む。自分のサーバの節では同じ関数を `bao` の呼び出しに替えるだけで、以後の手順を共有する。 - -`bao server -dev` をホストで起動する形は、ホストへ `bao` を入れる手順と、root token を画面から写す手順が要る。`compose.yml` の `image` の 1 行に版を書けば、版を 1 か所に保てる(I5)。 - -根拠: Value 1 / Mission(MVV 版 1) - -### 決定 6: 利用者ごとの権限を他の利用者の設定に触れずに変えられるようにするため、雛形のポリシーを利用者の entity へ直に付け、identity group を使わない - -利用者 1 人につき、AppRole のロール(`token_policies=default`)・entity(名前は利用者の識別子、`policies` に雛形のポリシー)・entity alias(名前は `role_id`、AppRole のマウントの accessor)を作る。チーム共通への書き込みを許すのは、その利用者の entity の `policies` に書き込みのポリシーを足すことである。個人の形も同じ手順で、`personal` の書き込みのポリシーを付ける。 - -identity group で束ねると、メンバーの一覧を書き直す操作が他の利用者の権限を巻き込み、1 人を足すときに既存のメンバーを落とし得る。ロールの `token_policies` に付ける形でも権限は効くが、1 人の権限がロールと entity の 2 か所に分かれ、チーム共通への書き込みを誰に許したかを entity の一覧だけで読めなくなる。 - -根拠: Value 3(MVV 版 1) - -### 決定 7: `version: 2` の雛形をグループ名の書き換えだけで使えるようにするため、グループごとのポリシーを置き換え語 `__GROUP__` から作る - -OpenBao のポリシーのテンプレートは、entity の属するグループを列挙してパスへ入れる書き方を持たない。`team/__GROUP__/` を書いた雛形を `sed` で置き換え、`devbase-team-read-` / `devbase-team-write-` として書き込む。個人の雛形は `users/{{identity.entity.name}}/` の下を許すため、`version: 1` と `2` で 1 つを共有する。 - -entity の metadata にグループ名を持たせて `{{identity.entity.metadata.…}}` で組む形は、1 人が複数のグループに属せず、チームの形で 2 つのグループを持つ利用者を表せない。 - -根拠: Value 3(MVV 版 1) - -### 決定 8: 試用で既存の接続設定を失わないため、試用の前に `secrets/backend.yml` と `secrets/bootstrap.env.age` を退避し、片付けで戻す - -`devbase env backend use openbao` は 2 つのファイルを上書きする。既存のサーバの形の利用者が試用の構成を試すと、今の接続設定と資格情報を失う。片付けは、退避があれば戻し、無ければ `devbase env backend use auto` で設定なしへ戻して試用の `bootstrap.env.age` を消す。どちらも最後に `devbase env backend status` で OpenBao を指していないことを確かめる(受け入れ条件 7)。手元のキャッシュ(`secrets/cache/`)は、退避を戻すときは消さない(元の接続の控えが入っており、試用の控えは接続先と `role_id` の指紋が違うため元の接続では使われない)。`use auto` で戻すときは、控えが試用のものだけなので丸ごと消す。 - -試用を別の `DEVBASE_ROOT` で行う形は、作業用のプロジェクトとプラグインを別に用意することになり、数分で試せる構成にならない。 - -根拠: Value 2 / Value 3(MVV 版 1) - -### 決定 9: 失効させる端末を `secret_id` の値を持たずに選べるようにするため、端末の名前を `secret_id` の metadata に入れて発行し、失効は accessor で行う - -発行のときに `metadata='{"terminal":"<端末の名前>"}'` を付け、失効は `secret-id` の一覧(accessor)と `secret-id-accessor/lookup` で端末の名前を確かめてから `secret-id-accessor/destroy` で行う。管理する人は発行した `secret_id` の値を控えずに済む。 - -`secret_id` の値で失効させる形は、管理する人が全端末の `secret_id` を保管することになり、漏れたときの範囲が広がる。 - -根拠: Value 3(MVV 版 1) - -## テスト設計 - -| 受け入れ条件・不変条件 | どの振る舞いで縛るか | どう壊したら落ちるべきか | -| --- | --- | --- | -| 受け入れ条件 1 | サーバの用意の文書に、個人・チーム・既存のサーバの 3 つの見出しと、形ごとに用意するものの表がある(レビュー) | 見出しを 2 つにまとめると落ちる | -| 受け入れ条件 2 | 個人とチームの手順が、マウント・AppRole・ポリシーの書き込み・entity と alias・ロール・`secret_id` を `bao` のコマンドで順に並べ、実機で上から打って通る(手動確認) | entity の名前を利用者の識別子(`--user`)と違えて作ると、本人の置き場が 403 になって打鍵で止まる | -| 受け入れ条件 3 | チームの節の、利用者と端末を足す・失効させる手順が実機で通る(手動確認。受け入れ条件 15 と同じ打鍵) | accessor の取り違えで別の端末が失効すると落ちる | -| 受け入れ条件 4・I4・I5・I6 | 雛形と試用の構成の検査(pytest): 5 つの雛形がある、どの `path` も `devbase/data/` か `devbase/metadata/` で始まる、`sys/` `auth/` `identity/` を含まない、`compose.yml` の版が `BAO_VERSION` と 2 つ目まで同じ、ポートが `127.0.0.1:` で始まる。`bao policy write` で拒まれないことは実機(手動確認) | `path "sys/*"` を足す、`image` の版を 2.5 にする、ポートを `8200:8200` にすると落ちる | -| 受け入れ条件 5・I3 | `version: 2` の雛形の `__GROUP__` を `personal` に置き換えると、`team/personal/` の下だけを指す(pytest)。組織の名前が無いことは固有の語の検査 | `team/*` を書くと落ちる | -| 受け入れ条件 6・7 | 試用の構成の起動・確かめ(`bao status`)・片付けの後の `devbase env backend status` が OpenBao を指さないことを、作業用の端末で通す(手動確認) | 片付けで退避を戻し忘れると、`status` が試用の URL を指して落ちる | -| 受け入れ条件 8・I7・I8・I9 | `_bao_environment` と生成した構成(pytest): 入出力の契約の表の各行、`extra_hosts` のリストと辞書の形、既にある `host.docker.internal` の行。実機ではコンテナの中の `bao kv get` が本人の個人共通を読める(手動確認) | 置き換えを常に行う・リモート扱いでも行う・`https` でも行う・行を重ねて足す・`openbao.url` 自体を書き換えると落ちる | -| 受け入れ条件 9・10・11 | `grep -rn '社内メールアドレス\|運用側のリポジトリ' docs/user docs/specifications/secret-backend.md` が 0 件。`env-backend.md` の「管理者から」が既存のサーバの形の記述だけにある(レビュー) | 語を 1 つ残すと grep が当たる | -| 受け入れ条件 12 | 確定仕様の「OpenBao との契約」に、サーバに求める条件の表と雛形の置き場がある(レビュー) | 条件の 1 つ(`users/` 直下の一覧の拒否など)を落とすとレビューで落ちる | -| 受け入れ条件 13 | 作業用のプロジェクトで、試用の構成から `env set` → `env get` → `devbase up` を通し、コンテナの環境変数に値がある(手動確認。非対話で base を建てられなければ「判断が要る」で返す) | 文書の手順のどこかが欠けると打鍵が止まる | -| 受け入れ条件 14・I1・I2 | `version: 2` と `version: 1` の雛形を書き込んだサーバで、`member01`・`member02` の読み書きが 403 / 通る の組になる(手動確認) | 個人の雛形の `{{identity.entity.name}}` を `*` にする、読み取りの雛形に `update` を足すと落ちる | -| 受け入れ条件 15 | `member02` の端末 A の `secret_id` を accessor で失効させると、A の `devbase env backend test` がログインの失敗で止まり、端末 B は通る(手動確認) | ロールごと消すと B も止まって落ちる | -| 受け入れ条件 16 | `https` の `openbao.url` で、生成した構成の `BAO_ADDR` と `extra_hosts` が今と同じ(pytest)。`env-backend.md` の接続の手順のコマンドと引数が変わらない(レビュー) | `https` の URL を書き換えると落ちる | -| 受け入れ条件 17・18 | `env -u DEVBASE_ROOT uv run --locked pytest tests/ -q` と、語の一覧のある端末での `proper_term_check.py` | — | -| I10 | 文書の検査(pytest): サーバの用意の文書と `env-backend.md` に、`--secret-id` の後に値を置く形・`BAO_TOKEN=` に値を置く形・`root` の token の値が無い | 手順に `--secret-id xxxx` を書くと落ちる | - -## 設計の結果 - -| 課題 | 扱い | 取り込み先 | 触るファイル | -| --- | --- | --- | --- | -| #295 | 実装する | — | `docs/user/openbao-server.md`、`docs/user/openbao/`、`docs/user/env-backend.md`、`docs/specifications/secret-backend.md`、`docs/README.md`、`docs/glossary/glossary.json`、`docs/glossary.md`、`lib/devbase/commands/container.py`、`lib/devbase/volume/compose.py`、`tests/commands/test_container_bao.py`、`tests/volume/`、`tests/docs/`、`CHANGELOG.md` | - -確定仕様(`secret-backend.md`)で書き直す箇所は、393 行(グループのポリシーの持ち主)・922 行(利用者の識別子)・926-929 行(サーバ側の構成)・974 行(`bao` を入れる理由の「管理スクリプト」)・「コンテナの中の `bao`」の `BAO_ADDR` の行・1418-1420 行(運用の「範囲外」)・1652 行(関連リンク)と、「テスト観点」の手動確認の行である。`env-backend.md` は 36-47 行・133-134 行・「コンテナの中から `bao` を使う」の表・578 行の注釈・607 行を書き直す。 - -## 未確認のまま残ること - -| 項目 | 内容 | -| --- | --- | -| Linux の Docker でのコンテナの中の `bao` | 試用の構成はポートをホストの `127.0.0.1` にだけ公開する。Linux の Docker では、コンテナから `host-gateway`(ブリッジのゲートウェイ)宛ての要求が、`127.0.0.1` にだけ公開したポートへ届かない見込みである。実装で Linux の Docker の端末を使えれば確かめ、届かなければ文書に「Linux の Docker では試用の構成でコンテナの中の `bao` は使えない。使うにはサーバをブリッジから届くアドレスでも待たせる」と書く。使えなければ確かめた環境(macOS の Docker Desktop)を文書に書く | -| `network_mode: host` の dev サービス | 手元のプラグインに `network_mode` を使う dev サービスは無い(`repos/*/` を探した。プラグインの中のプロジェクトのコードの compose は devbase が生成しない)。Linux でこの形の dev サービスでは loopback がそのまま届くが、置き換えで `host.docker.internal` 宛てになる。実装で `network_mode: host` の dev サービスには置き換えないかを、生成の側で判定できるか確かめる | -| 本番のサーバの構築 | 冗長化・バックアップ・監査ログ・TLS の証明書・seal の扱いは範囲外で、OpenBao の公式の文書へのリンクにとどめる。自分のサーバの節は、起動と初期化が済んだサーバを入口の前提にする | -| `secret_id` の受け渡しの経路 | チームの形で、管理する人が発行した `secret_id` を利用者へ渡す経路(応答の包み込み・対面など)は文書の範囲外とし、「安全な経路で渡す」とだけ書く | diff --git a/issues/issue-295-requirements.md b/issues/issue-295-requirements.md deleted file mode 100644 index f75db232..00000000 --- a/issues/issue-295-requirements.md +++ /dev/null @@ -1,244 +0,0 @@ -# #295: OpenBao backend を、利用者が個人・チームで自分のサーバを立てて使う前提の文書と雛形に揃える - -正は課題の本文(#295)で、この文書はその写しである。`spec-copy.py write` で作り直す。手で直さない。 - -## 何をしたいか - -devbase の OpenBao backend を、**利用者が個人またはチームで自分の OpenBao サーバを立てて使う**前提で使えるようにする。 - -今の文書と設計は、ある 1 つの組織が運用しているサーバへ接続することを前提に書かれている。OSS として多くの人が使うなら、サーバの用意から devbase の接続までを、利用者が公開の情報だけで再現できる必要がある。 - -## 今どうなっているか - -`main` = `2ee7e27`(2026-10-06)で確認した。 - -| 場所 | 現状 | OSS の利用者にとっての問題 | -| --- | --- | --- | -| `docs/user/env-backend.md:34-48`「OpenBao 側で用意するもの」 | 「サーバの構築と運用は devbase の範囲外です。管理者から次の値を受け取ってください」 | 管理者のいない個人や小さなチームは、何を用意すればよいか分からない | -| `docs/user/env-backend.md:42`、`docs/specifications/secret-backend.md:922` | 利用者の識別子を「社内メールアドレスの `@` より前」と説明している | 特定の組織の運用の決まりで、一般の前提ではない | -| `docs/specifications/secret-backend.md:393,926-929,1418,1652` | サーバ側の構成(KV v2 のマウント、ポリシー、AppRole、token の期限)は「運用側のリポジトリ」が持つとし、サーバの構築・運用は devbase の範囲外としている | devbase が前提にしているポリシー(本人だけが `users//` を読み書きできる、チーム共通は書き込み権限を持つ人だけ、`version: 2` ではグループ単位)が公開されておらず、再現できない | -| `lib/devbase/env/backend_config.py:126-141` | URL は `https`、または loopback 宛ての `http` だけを受け付ける | 個人で `bao server -dev` を手元で動かす形は通る。ただし下の「確かめること」のとおり、コンテナの中からは同じ URL で届かない疑いがある | -| `lib/devbase/cli.py:522-546`、`lib/devbase/env/openbao.py` | 認証は AppRole(`role_id` / 端末ごとの `secret_id`)だけ | 個人の 1 台の利用でも、AppRole の有効化・ロール・ポリシーを自分で組む必要がある。手順が無い | - -## 提案 - -| 項目 | 内容 | -| --- | --- | -| 利用の形 | 次の 3 つを文書で分けて示す。(1) **個人**: 手元または自分のサーバに 1 台立てて 1 人で使う。(2) **チーム**: チームで 1 台立て、チーム共通と個人の置き場を分ける。(3) **既存のサーバ**: 組織が運用しているサーバへ接続する(今の文書の形) | -| サーバの用意の手順 | KV v2 のマウント(既定 `devbase`)、AppRole の有効化、利用者ごとのロールと端末ごとの `secret_id` の発行、entity と alias の作成を、`bao` のコマンドで順に示す | -| ポリシーの雛形 | devbase が前提にする権限(本人の `users//`、チーム共通の読み取りと書き込み、`version: 2` のグループ単位の `team//…`。例: `team/personal/…`)を HCL の雛形としてリポジトリに置く(例 `docs/user/openbao/` や `etc/openbao/`)。`version: 1` と `version: 2` の両方 | -| 最小の試し方 | 個人が数分で試せる構成(`bao server -dev` か、docker compose の 1 ファイル)と、終わったら片付ける手順 | -| 利用者の識別子 | 「社内メールアドレス」ではなく「サーバの entity 名と同じ識別子」と説明する | -| 前提の書き直し | 確定仕様から非公開のリポジトリへの参照を外し、devbase がサーバに求める条件(マウント・パス・ポリシー・認証)を仕様の側に書く。`secret-backend.md:393,926-929,1418,1652` と、利用者の識別子を「社内メールアドレスの `@` より前」とする `secret-backend.md:922` の書き直しはこの課題が持つ(置き換えではなく、前提の書き直しのため #294 に含めない) | - -## 確かめること - -- `openbao.url` を `http://127.0.0.1:8200` にしたとき、`devbase up` がコンテナへ渡す `BAO_ADDR`(`lib/devbase/commands/container.py:921-932` の `_bao_environment`)がコンテナの中から届くか。`_bao_environment` は `openbao.url` をそのまま `BAO_ADDR` に入れ、loopback の宛先を書き換えないため、コンテナの中の `127.0.0.1` はコンテナ自身を指す。届かないなら、個人の手元のサーバの形では、コンテナの中の `bao` が使えない -- 個人の 1 人の利用で、`version: 2`(グループ別の置き場)と `group_aliases` が要るか。要らないなら、個人の形の既定をどうするか。アカウントグループの宣言は必須で、`default` は予約語として弾かれる(#315)。backend を問わず各プロジェクトの `env` に `DEVBASE_ACCOUNT_GROUP` が要るため、個人の形で宣言するグループ名(例: `personal`)を合わせて決める -- AppRole のほかに、個人の形で token による認証を足す価値があるか(足すなら、token の保存先と期限切れの扱い) -- 雛形のポリシーで、今の受け入れ条件(他人のパスは 403、チーム共通への書き込みは権限のある人だけ)が満たされるか。実機の OpenBao(2.6 系)で確かめる - -## 決めること - -- **devbasex が共通の OpenBao サーバを 1 台運用する案は採らない(2026-09-26 に決定)。** 利用者が個人またはチームで自分のサーバを立てる形(セルフホスト)を前提にする。試用のための共通サーバも、この課題では作らない。理由は次の 4 つ - - 全利用者の機密が 1 か所に集まり、侵害されたときの被害が全利用者に及ぶ。その責任を OSS の運営が負う - - 利用者と端末の資格情報の発行・失効、濫用への対応、可用性、費用が続く。ツールの開発ではなくサービスの運営になる - - 機密を自前で持ちたいから OpenBao を選ぶ利用者が多い。手順と雛形があれば、同じ構成を誰でも再現できる - - 多数の他人で 1 台を共有すると、ポリシーの誤りがそのまま他人の機密の漏れになる -- ポリシーの雛形とサーバを試す構成をどこに置くか(`docs/` か、配布物に含めるか) -- 個人・チーム・既存のサーバの 3 つの形を、1 つの文書にまとめるか分けるか -- `devbase env backend use openbao` に、個人の形の初期設定を助ける機能(例: ポリシーの雛形を書き出す副命令)を足すか、文書だけにするか - -## 完了条件 - -- 管理者のいない個人が、公開の文書と雛形だけで OpenBao サーバを立て、`devbase env backend use openbao` で接続し、`env set` / `env get` / `devbase up` の機密の読み込みまで通せる -- チームで 1 台立てたとき、本人以外の個人の置き場が 403 になり、チーム共通への書き込みが権限のある人だけに限られることを、雛形のポリシーで確かめてある -- 利用者向けの文書と確定仕様が、特定の組織のサーバや運用の決まりを前提にしていない - -## 関連 - -- #273(閉じた)— `devbase list` の TUI の「OpenBao の接続設定」(`lib/devbase/tui/actions_env_openbao.py`)が入った。この画面は token を入力させず保存もしない。個人の形で token の認証を足すなら、この画面と揃える -- #294(閉じた)— 固有の情報の除去。組織のグループ名の置き換えは #294、サーバの前提の書き直しはこの課題 -- #315(閉じた)— 前提 10 の `team/personal/…` のポリシーを、公開の雛形としてこの課題が示す。#315 の完了はこの課題を待たない - -## 由来 - -利用者からの依頼(2026-09-26)。「OSS として多くの人が利用する場合は、利用者が個人・チームでそれぞれのサーバを立てる前提での運用が必要」。 - -## 依頼(原文) - -> OSS として多くの人が利用する場合は、利用者が個人・チームでそれぞれのサーバを立てる前提での運用が必要 - -(書き手: 利用者、日付: 2026-09-26。本文の「由来」の節が引いた依頼の文) - -> devbasex は共通の OpenBao サーバを運用しない。利用者が個人またはチームで自分のサーバを立てる形(セルフホスト)を前提にする -> 試用のための共通サーバも、この課題では作らない。要るなら別の課題で扱う - -(書き手: 起票者が利用者の決定を記録したコメント、日付: 2026-09-26。決定の記録として、利用者が決めた条件に扱う) - -> 個人の形も `version: 2`(グループ別の置き場)にそろえ、宣言するアカウントグループの名前は `personal` にする。形が 1 つで済み、後からチームの形へ移りやすい -> 認証は AppRole だけにする。token による認証は足さない(保存先と期限切れの扱いを持ち込まない) - -(書き手: 起票者が利用者の決定を記録したコメント「決定(2026-10-11 利用者)」、日付: 2026-10-11。同上) - -上の 3 つのほかの本文(何をしたいか・今どうなっているか・提案・確かめること・決めること・完了条件)は起票者が書いた。 -利用者の指示の原文としては扱わず、原文に無い条件は次の「起票者の推定」に写す。 - -## 起票者の推定 - -題・本文のうち、利用者の指示の原文に無く起票者が推して足した条件。承認ゲート 1 で利用者が確かめるまで、利用者の決定として扱わない。 - -| # | 条件 | 推した出所 | この要求での扱い | -| --- | --- | --- | --- | -| S1 | 利用の形を「個人」「チーム」「既存のサーバ」の 3 つに分けて文書で示す | 本文「提案」の「利用の形」 | 受け入れ条件 1 | -| S2 | サーバの用意(KV v2 のマウント・AppRole の有効化・ロールと端末ごとの `secret_id`・entity と alias)を `bao` のコマンドで順に示す | 本文「提案」の「サーバの用意の手順」 | 受け入れ条件 2・3 | -| S3 | ポリシーの雛形を HCL のファイルとしてリポジトリに置き、`version: 1` と `version: 2` の両方を用意する | 本文「提案」の「ポリシーの雛形」 | 受け入れ条件 4・5。置き場は未決 1 | -| S4 | 個人が数分で試せる構成(`bao server -dev` か docker compose の 1 ファイル)と、片付けの手順を示す | 本文「提案」の「最小の試し方」 | 受け入れ条件 6・7 | -| S5 | 利用者の識別子を「社内メールアドレスの `@` より前」でなく「サーバの entity 名と同じ識別子」と説明する | 本文「提案」の「利用者の識別子」 | 受け入れ条件 10 | -| S6 | 確定仕様から非公開のリポジトリ(運用側のリポジトリ)への参照を外し、devbase がサーバに求める条件を仕様に書く | 本文「提案」の「前提の書き直し」 | 受け入れ条件 11・12 | -| S7 | 管理者のいない個人が、公開の文書と雛形だけで `env set` / `env get` / `devbase up` の機密の読み込みまで通せる | 本文「完了条件」1 | 受け入れ条件 13 | -| S8 | チームで 1 台立てたとき、他人の個人の置き場が 403、チーム共通への書き込みが権限のある人だけになることを雛形で確かめる(実機の OpenBao 2.6 系) | 本文「完了条件」2・「確かめること」4 | 受け入れ条件 14 | -| S9 | 利用者向けの文書と確定仕様が、特定の組織のサーバや運用の決まりを前提にしない | 本文「完了条件」3 | 受け入れ条件 9・11 | -| S10 | `openbao.url` が loopback のとき、コンテナの中の `bao` が同じ URL で届くかを確かめる | 本文「確かめること」1 | 受け入れ条件 8。届かせる手段は未決 4 | -| S11 | `devbase env backend use openbao` に、個人の形の初期設定を助ける機能(雛形を書き出す副命令など)を足すかを決める | 本文「決めること」4 | 未決 3 | - -## 目的 - -- 組織の管理者がいない個人や小さなチームが、公開されている文書と雛形だけで自分の OpenBao サーバを立て、 - devbase の OpenBao backend で機密を扱えるようにする -- 利用者向けの文書と確定仕様から、特定の組織が運用するサーバと運用の決まりを前提にした記述を無くす - -## 前提 - -- 前提 1: devbasex は共通の OpenBao サーバを運用せず、試用のための共通サーバも作らない(2026-09-26 の決定) -- 前提 2: 認証は今の AppRole(利用者ごとの `role_id`・端末ごとの `secret_id`)だけで、token による認証は足さない(2026-10-11 の決定) -- 前提 3: 個人の形も `version: 2`(`openbao.layout: group`)を使い、プロジェクトが宣言するアカウントグループは `personal` とする(2026-10-11 の決定)。個人の形で `group_aliases` は使わない -- 前提 4: 対象の OpenBao は base イメージの `bao` と同じ 2.6 系(`containers/base/Dockerfile` の `ARG BAO_VERSION=2.6.2`)とする。ほかの版での動作は確かめない -- 前提 5: devbase がサーバへ送る要求(4 経路のパス・KV v2 の API・AppRole のログイン)は変えない。サーバの側の雛形と文書を、今の devbase の要求に合わせる -- 前提 6: `bao server -dev` は機密を記憶にだけ置き、止めると消える。試用の構成は本物の機密を置く場所としては案内しない -- 前提 7: 実機の確認は、利用者の実際の環境の `devbase up`(P3)を使わず、作業用のプロジェクトと試用のサーバで行う。利用者の環境での確かめはリリース後テストで利用者が行う - -## 対象範囲 - -含む: -- 利用者向けの文書(`docs/user/env-backend.md` と、要れば新しい文書)の、サーバの用意から接続までの手順 -- OpenBao のポリシーの雛形(HCL)と、試用の構成(起動の手順か compose のファイル)をリポジトリに置くこと -- 確定仕様(`docs/specifications/secret-backend.md`)の、サーバの前提の書き直し(393・922・926-929・1418-1420・1652 行の付近) -- コンテナの中の `bao` が、個人の形の構成で使えるかの確認と、その結果に合わせた文書(届かせるためにコードを変えるかは未決 4) -- 用語集への語の追加 - -含まない: -- devbasex が運用する共通の OpenBao サーバ、試用のための共通サーバ(前提 1) -- token による認証(前提 2) -- サーバの冗長化・バックアップ・監査ログ・TLS の証明書の発行など、サーバの本番の運用の手順(公式の文書へのリンクにとどめる) -- 組織のグループ名などの固有の語の置き換え(#294 で済んでいる) -- base イメージの `bao` の版の変更(P1) -- devbase がサーバへ送る要求の変更(前提 5) - -## ドメインイベント - -| # | イベント | 引き金 | 失敗したとき | 順序の前提 | -| --- | --- | --- | --- | --- | -| E1 | 利用者が利用の形(個人・チーム・既存のサーバ)を選んだ | 利用者が文書を開いた | 形を選べない → 文書の冒頭で形の違いを示す(受け入れ条件 1) | — | -| E2 | 利用者が OpenBao サーバを起動した | 利用者が試用の構成か自分のサーバで起動の手順を打った | ポートが使われている・起動しない → 文書に確かめ方(`bao status`)を書く(受け入れ条件 6) | E1 | -| E3 | 管理する人が KV v2 を既定の `devbase` にマウントした | 手順のコマンド | 既にマウントがある → 手順に既存のマウントの扱いを書く | E2 | -| E4 | 管理する人が AppRole を有効にし、雛形のポリシーを書き込んだ | 手順のコマンド | HCL の誤り → `bao policy write` が拒む。雛形は 2.6 系で書き込めることを確かめておく(受け入れ条件 4) | E3 | -| E5 | 管理する人が利用者の entity・alias・ロールを作った | 利用者を足すとき | entity 名と devbase の `user` が食い違う → 個人の置き場が 403。文書に揃える旨を書く(受け入れ条件 10) | E4 | -| E6 | 管理する人が端末の `secret_id` を発行した | 端末を足すとき | — | E5 | -| E7 | 利用者が `devbase env backend use openbao` で接続した | 利用者のコマンド | URL の検査で止まる・ログインが失敗する → `devbase env backend test` で切り分ける | E6 | -| E8 | 利用者がプロジェクトで `DEVBASE_ACCOUNT_GROUP=personal` を宣言した | 利用者が `projects//env` を書いた | 宣言が無い → 機密のコマンドと `up` が止まる(今の振る舞い) | — | -| E9 | 利用者が `env set` / `env get` で機密を読み書きした | 利用者のコマンド | 他人の個人の置き場・権限の無いチーム共通への書き込み → 403(受け入れ条件 14) | E7・E8 | -| E10 | `devbase up` がコンテナへ機密を入れた | 利用者の `devbase up` | サーバに届かない → 今の振る舞い(キャッシュへ落ちるか止まる) | E9 | -| E11 | コンテナの中の `bao` がサーバへ届いた(または届かなかった) | 利用者がコンテナの中で `bao` を打った | loopback の URL が届かない → 未決 4 と受け入れ条件 8 | E10 | -| E12 | 管理する人が端末の `secret_id` を失効させた | 端末を手放したとき | — | E6 | -| E13 | 利用者が試用の構成を片付けた | 試し終えたとき | devbase の設定がサーバを指したまま残る → 機密のコマンドが不達で止まる。片付けに backend を戻す手順を含める(受け入れ条件 7) | E7 | - -## 用語 - -| 用語 | 意味 | -| --- | --- | -| 利用の形 | OpenBao サーバの持ち方の 3 つの区分。個人の形(1 人で 1 台)・チームの形(チームで 1 台、チーム共通と個人の置き場を分ける)・既存のサーバの形(組織が運用するサーバへ接続する) | -| ポリシーの雛形 | devbase が前提にする権限を OpenBao のポリシー(HCL)として書いた、リポジトリに置くファイル。利用者が自分のサーバへ書き込んで使う | -| 試用の構成 | 個人が手元で OpenBao を立てて devbase の接続を試すための、起動から片付けまでの最小の構成 | - -## 受け入れ条件 - -文書と雛形: - -- [ ] 1. 利用者向けの文書が、利用の形の 3 つ(個人・チーム・既存のサーバ)を見出しで分け、それぞれで利用者が何を用意するかを示している -- [ ] 2. 個人の形とチームの形の文書が、KV v2 のマウント(既定 `devbase`)・AppRole の有効化・ポリシーの書き込み・entity と alias の作成・利用者ごとのロールの作成・端末ごとの `secret_id` の発行を、`bao` のコマンドで順に示している -- [ ] 3. チームの形の文書が、利用者と端末を足す手順と、端末 1 台の `secret_id` を失効させる手順を `bao` のコマンドで示している -- [ ] 4. ポリシーの雛形がリポジトリに HCL のファイルとして置かれ、`version: 1` と `version: 2` の両方がある。どの雛形も OpenBao 2.6 系の `bao policy write` で拒まれない -- [ ] 5. `version: 2` の雛形は、グループの名前を書き換えるだけで `team//…` の置き場に使える(例として `personal` を示す)。雛形の中に特定の組織の名前が無い -- [ ] 6. 試用の構成が、個人が手元の 1 台で OpenBao を起動し、受け入れ条件 2 の手順を打ち、`devbase env backend use openbao` で接続するまでを示している。起動を確かめるコマンドと、止めると機密が消えること(前提 6)を書いている -- [ ] 7. 試用の構成の文書が片付けの手順を持ち、サーバを止めることと、devbase の backend を元へ戻すこと(`secrets/backend.yml` と `secrets/bootstrap.env.age` の扱い)を含んでいる。手順の後に `devbase env backend status` が OpenBao を指していない -- [ ] 8. 個人の形の文書が、コンテナの中の `bao` を使えるかと、使うときの `openbao.url` の書き方を明記している。使えると書いた構成では、dev コンテナの中の `bao kv get` が本人の個人共通の置き場を読める -- [ ] 9. `docs/user/env-backend.md` から「サーバの構築と運用は devbase の範囲外です。管理者から次の値を受け取ってください」の前提が消え、既存のサーバの形でだけ「管理者から受け取る」と書かれている -- [ ] 10. 利用者向けの文書と確定仕様が、利用者の識別子(設定の `user`)を「サーバの entity 名と同じ識別子」と説明し、「社内メールアドレス」の語が `docs/user/` と `docs/specifications/secret-backend.md` に無い -- [ ] 11. `docs/specifications/secret-backend.md` に「運用側のリポジトリ」の語が無く、関連リンクから運用側のリポジトリへの参照が消えている -- [ ] 12. `docs/specifications/secret-backend.md` が、devbase がサーバに求める条件(KV v2 のマウント・4 経路のパス・AppRole のログイン・本人のパスの読み書き・チームのパスの読み取り・チーム共通への書き込みは権限のある人だけ・他人のパスと `users/` 直下の一覧の拒否・`version: 2` のグループ単位の権限)を書き、ポリシーの雛形の置き場を示している - -実機(OpenBao 2.6 系): - -- [ ] 13. 前提: 管理者のいない端末。操作: 公開の文書と雛形だけを見て、試用の構成でサーバを立て、`devbase env backend use openbao` で接続し、`DEVBASE_ACCOUNT_GROUP=personal` を宣言した作業用のプロジェクトで `env set` → `env get` → `devbase up` を打つ。結果: `env get` が書いた値を返し、`up` したコンテナの環境変数にその値がある -- [ ] 14. 前提: `version: 2` の雛形を書き込んだサーバに、利用者 2 人(`member01`・`member02`)がいて、チーム共通への書き込みは `member01` にだけ許している。結果: `member02` の資格で `users/member01/…` の読み取りが 403、`member02` のチーム共通への書き込みが 403、`member01` のチーム共通への書き込みと両者のチーム共通の読み取りが通る。`version: 1` の雛形でも、他人の個人の置き場の 403 とチーム共通への書き込みの制限が同じく効く -- [ ] 15. 前提: 受け入れ条件 14 のサーバ。操作: `member02` の端末の `secret_id` を失効させる。結果: その端末の `devbase env backend test` がログインの失敗で止まり、`member02` の別の端末の `secret_id` ではログインが通る - -退行しないこと: - -- [ ] 16. 既存のサーバの形の利用者は、`secrets/backend.yml` と `secrets/bootstrap.env.age` を変えずに、これまでと同じコマンドで機密を読み書きできる(`env-backend.md` の接続の手順のコマンドと引数が変わらない) -- [ ] 17. `env -u DEVBASE_ROOT uv run --locked pytest tests/ -q` が通る -- [ ] 18. `python3 .github/scripts/proper_term_check.py` が「語の一覧が無いため飛ばした」と出さずに通る(固有の語の一覧のある端末で打つ) - -## 非機能の条件 - -| 大項目 | 条件 | -| --- | --- | -| セキュリティ | ポリシーの雛形は、受け入れ条件 12 に並べた権限のほかを許さない(本人以外の個人の置き場・`sys/` の管理の経路を利用者のポリシーに含めない)。文書の例に実の token・`secret_id`・root token の値を書かない。devbase の接続に root token を使う手順を示さない。`secret_id` をコマンド引数で渡す手順を示さない(今の `--secret-id-stdin` を使う) | -| 運用・保守性 | 雛形と試用の構成は OpenBao の版を 1 か所に書き、base イメージの `BAO_VERSION` と同じ 2.6 系に揃える | -| システム環境 | 試用の構成は macOS の Docker Desktop と Linux の Docker の両方で動く手順を示す。片方でしか確かめていなければ、文書に確かめた環境を書く | - -## 影響 - -| 対象 | 影響 | -| --- | --- | -| 公開インタフェース | 変わらない。初期設定を助ける副命令を足すかは未決 3 で、足すなら追加だけで既存のコマンドは変えない | -| データ | 変わらない。`secrets/backend.yml` の形とサーバ上のパスは変えない | -| 既存の振る舞い | 文書と確定仕様の記述が変わる。コンテナの `BAO_ADDR` の扱いを変える場合(未決 4)は、loopback の `openbao.url` を使う利用者のコンテナの中の `bao` の宛先だけが変わる | - -## 検証手段 - -| 項目 | 手段 | -| --- | --- | -| テスト | `env -u DEVBASE_ROOT uv run --locked pytest tests/ -q` | -| 静的解析 | `uvx ruff check --select=E9,F63,F7,F82 lib`、`python3 .github/scripts/proper_term_check.py` | -| 文書の語 | `grep -rn '社内メールアドレス\|運用側のリポジトリ' docs/user docs/specifications/secret-backend.md` が 0 件(受け入れ条件 10・11) | -| 手動確認(マージ前) | 受け入れ条件 4・14・15: 作業用の端末で OpenBao 2.6 系を試用の構成で立て、雛形を書き込み、2 人の利用者の資格で読み書きと失効を打つ。コマンドと結果を PR の本文かコメントに残す | -| 手動確認(マージ前) | 受け入れ条件 8・13: 作業用のプロジェクト(実の利用者の環境ではない)で接続から `devbase up` とコンテナの中の `bao kv get` までを打ち、結果を残す。非対話のセッションで base を建てられないときは「判断が要る」で返す | -| 手動確認 | リリース後テストで、利用者が自分の端末で個人の形の文書を最初から辿り、受け入れ条件 13 と同じ結果になるかを見る | - -## 前提とする取り決め - -| 項目 | 参照先 / 決めたこと | -| --- | --- | -| プロジェクト構造 | `AGENTS.md` の「このリポジトリ」。利用者向けの文書は `docs/user/`、確定仕様は `docs/specifications/`。雛形の置き場は未決 1 | -| コーディング規約 | `AGENTS.md` の「書き方」。文書は日本語、用語は用語集の語。実機の記録は `member01` などの中立の名前で書く | -| テスト戦略 | 文書と雛形は手動確認(マージ前)で実機の OpenBao に当てる。コードを変える場合(未決 3・4)は pytest で入出力を固める。今の偽の OpenBao(`http.server`)のテストは変えない | - -## 境界 - -| 区分 | 内容 | -| --- | --- | -| 常に行う | 全体のテストと lint と固有の語の検査、`CHANGELOG.md` の `[Unreleased]` の記入(利用者向けの文書が変わるため)、用語集の更新と `render` | -| 確認してから行う | 新しい副命令などの公開インタフェースの追加(未決 3)、`BAO_ADDR` の書き換えなどコードの振る舞いの変更(未決 4)。どちらも承認ゲートで人が見る | -| 行わない | base イメージの `bao` の版の変更(P1)、実の利用者の環境での `devbase up`(P3)、共通サーバの用意、token の認証の追加、devbase がサーバへ送る要求の変更 | - -## 未決 - -| # | 項目 | 誰が決めるか | 期限 | -| --- | --- | --- | --- | -| 1 | ポリシーの雛形と試用の構成の置き場(`docs/` の下か、`etc/` など配布物の中か) | 設計 | 設計 PR | -| 2 | 利用の形の 3 つを 1 つの文書にまとめるか、形ごとに分けるか | 設計 | 設計 PR | -| 3 | `devbase env backend use openbao` に、雛形を書き出すなど個人の形の初期設定を助ける機能を足すか、文書だけにするか | 設計 | 設計 PR | -| 4 | `openbao.url` が loopback のとき、コンテナの中の `bao` を届かせる手段(`BAO_ADDR` の宛先を `host.docker.internal` などへ書き換えるか、文書で別の URL を案内するか、コンテナの中の `bao` は使えないと書くか)。サーバが loopback だけで待つ場合の Linux の Docker の扱いを含む | 設計(実機の確認の結果で決める) | 設計 PR | -| 5 | 試用の構成を `bao server -dev` にするか、docker compose の 1 ファイルにするか | 設計 | 設計 PR | From f425124917d62e9afaaf2dde6efded4528abf787 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Tue, 13 Oct 2026 00:55:07 +0900 Subject: [PATCH 3/3] =?UTF-8?q?Docs:=20#453=20=E3=81=AE=E4=B8=80=E8=87=B4?= =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E3=81=AE=E4=BD=99=E5=88=86=E3=81=AE?= =?UTF-8?q?=E6=AF=94=E3=81=B9=E6=96=B9=E3=82=92=E7=A2=BA=E5=AE=9A=E4=BB=95?= =?UTF-8?q?=E6=A7=98=E3=81=AB=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0111uDyVMhJts3mForKPjcUX --- .../specifications/cli-argument-resolution.md | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/docs/specifications/cli-argument-resolution.md b/docs/specifications/cli-argument-resolution.md index 9e02138e..197bfc8c 100644 --- a/docs/specifications/cli-argument-resolution.md +++ b/docs/specifications/cli-argument-resolution.md @@ -633,12 +633,16 @@ Python 側の `_resolve_project_name` は同じ結果になるよう、`chdir` 道の分岐の本体に値の補完を持たない `'--context[...]:context:'` があること。bash から道 1 つの `--context` を 外すと bash のテストが、zsh から外すと zsh のテストが、ファイルと道を挙げて落ちること (`tests/cli/test_name_context_consistency.py`) +- `--context` を取らない末端の道で、bash の `devbase <道> -` の候補に `--context` が出ないこと。末端の道の + 集合が `env set` を含み、`--context` の集合と重ならないこと。bash の末端の道(例 `env set`)の分岐へ + `--context` を足すと、その道を挙げて落ちること(`tests/cli/test_name_context_consistency.py`) - bash の `-` で始まる語の候補が、旗の候補に `--context` を足した集合であること。`--context` の直後の候補が 0 件であること(`devbase up --context`・`devbase project up web --context`・`devbase container ps --context`・ `devbase env exec --context`)。`etc/_devbase` の `_devbase()` の中に `_devbase_arguments` を通らない `_arguments` の呼び出しが無いこと(`tests/cli/test_completion.py`) -- zsh の補完が実際に出す候補が I1〜I5 のとおりであること。期待に合わない行を、打った行・期待・実際の候補を - 並べて挙げること。補完を読み込めなければ行ごとに並べる前に落ちること。応答しない補完を 10 秒(テストでは +- zsh の補完が実際に出す候補が I1〜I5・X1 のとおりであること。打つ行の一覧に `devbase env set -`(X1)が + 入ること。zsh の末端の道の分岐へ `--context` を足すと、X1 がその行を挙げて落ちること。期待に合わない行を、 + 打った行・期待・実際の候補を並べて挙げること。補完を読み込めなければ行ごとに並べる前に落ちること。応答しない補完を 10 秒(テストでは 短く渡す)で打ち切り、時間切れの行と流さなかった行を返し、起動したプロセスを残さないこと。利用者の rc を 読まないこと。zsh か `zsh/zpty` が無ければ、印が無ければ欠けたものの名前を出して飛ばし、印があれば落ちること (`tests/cli/test_zsh_completion_run.py`) @@ -646,7 +650,7 @@ Python 側の `_resolve_project_name` は同じ結果になるよう、`chdir` ## 決定の記録 `[name]` と `--context` を受け付けるサブコマンドの列挙の正本と一致テスト(#214)、補完の `--context`(#435)、 -zsh の補完の実行テスト(#455)の決定である。 +zsh の補完の実行テスト(#455)、補完の `--context` の余分の比べ方(#453)の決定である。 ### 決定 1: 取り残した写しを 1 回の実行で全部挙げるため、写しの場所ごとに別のテストにする @@ -758,6 +762,14 @@ zsh を必須にし、何のための値かが `ci.yml` から読めないため 静的な検査と、飛ばす範囲の境目を関数ごとに持つことになる。zsh を操作する部品は `tests/cli/zsh_completion_harness.py` に分け、テストのファイルには打つ行の一覧と比べ方だけを置く。 +### 決定 18: 補完の `--context` は欠けと余分の両方を比べ、余分は子の parser を持たない末端の道に限る + +補完が `--context` を取らない道で `--context` を勧めると、利用者が補完に従って打ったコマンドを argparse が +退け、終了コード 2 で止まる。欠けだけを比べる形では、写しが列挙の正本より広がっても検査で拾えない。余分を +比べる道を子の parser を持たない末端の道に限るのは、親の道(`project profile` など)の zsh の分岐の本体が +子の分岐を含み、子の `--context` を拾って誤って落ちるためである。zsh の余分は分岐の文字列でなく、補完の +実行テストの実際の候補で比べる(X1)。文字列で探すと、分岐の外の記述や子の分岐の記述で結果が変わる。 + ## 関連リンク - [CLI リファレンス: project](../user/cli-reference/02-project.md) @@ -769,3 +781,4 @@ zsh を必須にし、何のための値かが `ci.yml` から読めないため - 実装 PR: devbasex/devbase#428(#214。列挙の正本と一致テスト) - 実装 PR: devbasex/devbase#450(#435。補完の `--context`) - 実装 PR: devbasex/devbase#460(#455。zsh の補完の実行テスト) +- 実装 PR: devbasex/devbase#471(#453。補完の `--context` の余分の比べ方)