diff --git a/containers/base/entrypoint.sh b/containers/base/entrypoint.sh index b6dcebc6..ee5985b1 100644 --- a/containers/base/entrypoint.sh +++ b/containers/base/entrypoint.sh @@ -652,24 +652,14 @@ devbase_remove_vscode_server_path() { return 0 } -# 共通のボリュームの bin/ を片付ける (#463)。消すのは次の 3 種だけで、bin/ のほかの -# もの・~/.vscode-server の bin 以外には触らない。終了コードは常に 0。 -# -# 1. 残す版の数を超えた古い版。版は 40 桁の 16 進の名前で product.json を持つ -# ディレクトリ。product.json の更新時刻の新しい順 (同じなら名前の順) に並べる。 -# product.json は片付けが書き換えないため、同時に走る片付けどうしで並びが同じに -# なり、残る版が残す数より減らない (ディレクトリの更新時刻では減った) -# 2. 残った版の直下の入れ子の本体 (product.json を持つディレクトリ)。本体の同時の -# 展開で、遅れた側の中身が bin//vscode-server-linux-/ に残ったもの -# 3. 60 分より古い展開の途中のディレクトリ bin/_<数字> -# -# シンボリックリンクは辿らない。bin/ 自身・bin/・入れ子がリンクなら、版にも -# 片付けの対象にも数えない。リンク先の bin の外のものを消さないためである -devbase_prune_vscode_server_bin() { - local bin="$1" keep="${2:-$DEVBASE_VSCODE_SERVER_KEEP}" - local entry name mtime listing="" count=0 kept=() nested - - [ -d "$bin" ] && [ ! -L "$bin" ] || return 0 +# bin/ の版のうち、残す版の数を超えた古い版を消し、残した版の名前を新しい順に 1 行ずつ +# 標準出力へ出す (#463 の片付けの規則 1)。版は 40 桁の 16 進の名前で product.json を +# 持つディレクトリ。product.json の更新時刻の新しい順 (同じなら名前の順) に並べる。 +# product.json は片付けが書き換えないため、同時に走る片付けどうしで並びが同じになり、 +# 残る版が残す数より減らない (ディレクトリの更新時刻では減った) +devbase_prune_vscode_server_old_versions() { + local bin="$1" keep="$2" + local entry name mtime listing="" count=0 for entry in "$bin"/*; do name="${entry##*/}" @@ -684,13 +674,24 @@ devbase_prune_vscode_server_bin() { [ -n "$name" ] || continue count=$((count + 1)) if [ "$count" -le "$keep" ]; then - kept+=("$name") + printf '%s\n' "$name" else devbase_remove_vscode_server_path "${bin}/${name}" fi done < <(printf '%s' "$listing" | LC_ALL=C sort -k1,1nr -k2,2) - for name in "${kept[@]}"; do + return 0 +} + +# 残した版の直下の入れ子の本体 (product.json を持つディレクトリ) を消す (#463 の片付けの +# 規則 2)。本体の同時の展開で、遅れた側の中身が bin//vscode-server-linux-/ +# に残ったもの。残した版の名前は標準入力から 1 行ずつ読む +devbase_prune_vscode_server_nested() { + local bin="$1" + local name nested + + while IFS= read -r name; do + [ -n "$name" ] || continue # 数えた後にリンクへ置き換わっていれば、リンク先を辿らない [ ! -L "$bin" ] && [ ! -L "${bin}/${name}" ] || continue for nested in "${bin}/${name}"/*/; do @@ -701,6 +702,15 @@ devbase_prune_vscode_server_bin() { done done + return 0 +} + +# 60 分より古い展開の途中のディレクトリ bin/_<数字> を消す (#463 の片付けの +# 規則 3) +devbase_prune_vscode_server_partial() { + local bin="$1" + local entry name + while IFS= read -r entry; do name="${entry##*/}" [[ "$name" =~ ^[0-9a-f]{40}_[0-9]+$ ]] || continue @@ -711,6 +721,28 @@ devbase_prune_vscode_server_bin() { return 0 } +# 共通のボリュームの bin/ を片付ける (#463)。消すのは次の 3 種だけで、bin/ のほかの +# もの・~/.vscode-server の bin 以外には触らない。終了コードは常に 0。 +# +# 1. 残す版の数を超えた古い版 (devbase_prune_vscode_server_old_versions) +# 2. 残った版の直下の入れ子の本体 (devbase_prune_vscode_server_nested) +# 3. 60 分より古い展開の途中のディレクトリ (devbase_prune_vscode_server_partial) +# +# シンボリックリンクは辿らない。bin/ 自身・bin/・入れ子がリンクなら、版にも +# 片付けの対象にも数えない。リンク先の bin の外のものを消さないためである +devbase_prune_vscode_server_bin() { + local bin="$1" keep="${2:-$DEVBASE_VSCODE_SERVER_KEEP}" + local kept + + [ -d "$bin" ] && [ ! -L "$bin" ] || return 0 + + kept=$(devbase_prune_vscode_server_old_versions "$bin" "$keep") + devbase_prune_vscode_server_nested "$bin" <<< "$kept" + devbase_prune_vscode_server_partial "$bin" + + return 0 +} + # =================================================================== # PLAN39: GCP の認証モードと gcloud / gws の設定ディレクトリ # =================================================================== diff --git a/lib/devbase/commands/container.py b/lib/devbase/commands/container.py index b8b35bc1..89dfc5aa 100644 --- a/lib/devbase/commands/container.py +++ b/lib/devbase/commands/container.py @@ -1505,7 +1505,7 @@ def _run_deploy_pipeline(project_name: str, scale: int, config, 起動できなかったインスタンスがあれば、起動の待ちの ``ContainerStartupError`` をそのまま通す。 """ logger.info("[1/6] Ensuring volumes exist...") - ensure_volumes(scale, project_name) + ensure_volumes(scale) logger.info("[1.5/6] Ensuring network exists...") ensure_network('devbase_net') @@ -1945,7 +1945,7 @@ def _run_scale_pipeline(project_name: str, new_scale: int, current_scale: int, project_runtime.write_scale(Path.cwd(), new_scale) logger.info("[2/5] Ensuring volumes exist for scale=%d...", new_scale) - ensure_volumes(new_scale, project_name) + ensure_volumes(new_scale) logger.info("[2.5/5] Ensuring network exists...") ensure_network('devbase_net') diff --git a/lib/devbase/volume/manager.py b/lib/devbase/volume/manager.py index b8eec8be..2b445a9f 100644 --- a/lib/devbase/volume/manager.py +++ b/lib/devbase/volume/manager.py @@ -158,16 +158,6 @@ def find_legacy_vscode_volumes() -> list[str]: class VolumeManager: """Manages Docker volumes for devbase projects""" - def __init__(self, project_name: str = None): - """ - Initialize VolumeManager - - Args: - project_name: Project name. ボリュームの名前には使わない - (#463 より前は VS Code Server ボリュームの名前に使った) - """ - self.project_name = project_name - def _volume_exists(self, volume_name: str) -> bool: """Check if Docker volume exists""" try: @@ -291,8 +281,7 @@ def _ensure_volume(self, volume_name: str, raise DockerError(f"Failed to create volume {volume_name}") -def ensure_volumes(scale: int, project_name: str = None, - group: Optional[str] = None) -> None: +def ensure_volumes(scale: int, group: Optional[str] = None) -> None: """ Ensure required shared volumes exist for the specified scale @@ -303,10 +292,9 @@ def ensure_volumes(scale: int, project_name: str = None, Args: scale: Number of container instances - project_name: Project name (unused for volume names since #463) group: Account group name (省略時は DEVBASE_ACCOUNT_GROUP。既定の値は無い) """ - manager = VolumeManager(project_name) + manager = VolumeManager() manager.ensure_volumes(scale, group) diff --git a/tests/commands/test_container_scale_order.py b/tests/commands/test_container_scale_order.py index f13844cb..0b013386 100644 --- a/tests/commands/test_container_scale_order.py +++ b/tests/commands/test_container_scale_order.py @@ -85,7 +85,7 @@ def write_scale(project_dir, scale): monkeypatch.setattr(container.project_runtime, 'write_scale', write_scale) monkeypatch.setattr(container, 'ensure_volumes', - lambda scale, project: calls.append(('volumes', scale))) + lambda scale: calls.append(('volumes', scale))) monkeypatch.setattr(container, 'ensure_network', lambda name='devbase_net': calls.append(('network', name))) @@ -373,14 +373,17 @@ def test_scale_rejects_a_scale_not_above_current(scale_harness, new_scale): def test_scale_passes_explicit_project_name(scale_harness, monkeypatch): - """現状固定: 明示的に指定された project_name が ensure_volumes, _build_scaled_override, _push_bao_token に渡る。""" + """現状固定: 明示的に指定された project_name が _build_scaled_override, _push_bao_token に渡る。 + + ensure_volumes にはプロジェクト名を渡さない(ボリュームの名前に使わないため)。 + """ captured: dict = {} orig_volumes = container.ensure_volumes - def fake_ensure_volumes(scale, project): - captured['volumes_project'] = project - return orig_volumes(scale, project) + def fake_ensure_volumes(*args, **kwargs): + captured['volumes_args'] = (args, kwargs) + return orig_volumes(*args, **kwargs) orig_build = container._build_scaled_override @@ -400,7 +403,7 @@ def fake_push_bao(project_name, *args, **kwargs): assert container.cmd_scale(2, project_name='custom-proj') == 0 - assert captured['volumes_project'] == 'custom-proj' + assert captured['volumes_args'] == ((2,), {}) assert captured['build_project'] == 'custom-proj' assert captured['bao_project'] == 'custom-proj' diff --git a/tests/volume/test_manager_ensure_volumes.py b/tests/volume/test_manager_ensure_volumes.py index c6ac3a01..84cc2efa 100644 --- a/tests/volume/test_manager_ensure_volumes.py +++ b/tests/volume/test_manager_ensure_volumes.py @@ -51,7 +51,7 @@ def test_creates_all_volumes_in_order_when_none_exist(monkeypatch, caplog): calls = _fake_docker(monkeypatch) with caplog.at_level(logging.INFO, logger=manager.logger.name): - manager.VolumeManager("proj-a").ensure_volumes(2) + manager.VolumeManager().ensure_volumes(2) assert calls == [ ("exists", "devbase_home_ubuntu"), @@ -81,7 +81,7 @@ def test_existing_volumes_are_only_checked(monkeypatch, caplog): calls = _fake_docker(monkeypatch, existing=existing) with caplog.at_level(logging.INFO, logger=manager.logger.name): - manager.VolumeManager("proj-a").ensure_volumes(1, group="team-b") + manager.VolumeManager().ensure_volumes(1, group="team-b") assert calls == [ ("exists", "devbase_home_ubuntu"), @@ -103,7 +103,7 @@ def test_mixed_existing_and_missing(monkeypatch, caplog): calls = _fake_docker(monkeypatch, existing=existing) with caplog.at_level(logging.INFO, logger=manager.logger.name): - manager.VolumeManager("proj-a").ensure_volumes(3) + manager.VolumeManager().ensure_volumes(3) assert [name for kind, name in calls if kind == "create"] == [ "devbase_home_ubuntu", @@ -125,7 +125,7 @@ def test_mixed_existing_and_missing(monkeypatch, caplog): def test_scale_zero_creates_only_shared_volumes(monkeypatch): calls = _fake_docker(monkeypatch) - manager.VolumeManager("proj-a").ensure_volumes(0) + manager.VolumeManager().ensure_volumes(0) assert [name for kind, name in calls if kind == "create"] == [ "devbase_home_ubuntu", @@ -147,7 +147,7 @@ def test_failed_creation_raises_and_stops(monkeypatch, failing, created_before): calls = _fake_docker(monkeypatch, failing={failing}) with pytest.raises(DockerError) as excinfo: - manager.VolumeManager("proj-a").ensure_volumes(3) + manager.VolumeManager().ensure_volumes(3) assert str(excinfo.value) == f"Failed to create volume {failing}" assert calls[-1] == ("create", failing) @@ -158,7 +158,7 @@ def test_invalid_group_fails_before_touching_docker(monkeypatch): calls = _fake_docker(monkeypatch) with pytest.raises(DevbaseError): - manager.VolumeManager("proj-a").ensure_volumes(1, group="bad/name") + manager.VolumeManager().ensure_volumes(1, group="bad/name") assert calls == [] @@ -166,7 +166,7 @@ def test_invalid_group_fails_before_touching_docker(monkeypatch): def test_module_function_delegates_with_group(monkeypatch): calls = _fake_docker(monkeypatch) - manager.ensure_volumes(1, project_name="proj-a", group="team-b") + manager.ensure_volumes(1, group="team-b") assert [name for kind, name in calls if kind == "create"] == [ "devbase_home_ubuntu", diff --git a/tests/volume/test_manager_vscode.py b/tests/volume/test_manager_vscode.py index 5c042733..e2f65a85 100644 --- a/tests/volume/test_manager_vscode.py +++ b/tests/volume/test_manager_vscode.py @@ -50,12 +50,12 @@ def _record_docker(monkeypatch, existing: set[str] = frozenset()) -> list[str]: return created -def test_shared_volume_is_created_once_for_any_project_group_and_scale(monkeypatch): - """プロジェクト・グループ・scale が違っても、作るのは同じ名前の 1 本だけ。""" +def test_shared_volume_is_created_once_for_any_group_and_scale(monkeypatch): + """グループ・scale が違っても、作るのは同じ名前の 1 本だけ。""" created = _record_docker(monkeypatch) - manager.ensure_volumes(2, project_name="proj-a") - manager.ensure_volumes(1, project_name="proj-b", group="team-b") + manager.ensure_volumes(2) + manager.ensure_volumes(1, group="team-b") vscode = [n for n in created if "vscode" in n] assert vscode == [manager.VSCODE_SERVER_VOLUME] @@ -66,7 +66,7 @@ def test_existing_shared_volume_is_not_recreated(monkeypatch): created = _record_docker( monkeypatch, existing={manager.VSCODE_SERVER_VOLUME}) - manager.ensure_volumes(1, project_name="proj-a") + manager.ensure_volumes(1) assert manager.VSCODE_SERVER_VOLUME not in created @@ -75,7 +75,7 @@ def test_no_per_instance_vscode_volume_is_created(monkeypatch): """インスタンスごとの VS Code Server のボリュームと旧形式を作らない。""" created = _record_docker(monkeypatch) - manager.ensure_volumes(3, project_name="proj-a") + manager.ensure_volumes(3) assert not [n for n in created if n.startswith(manager.LEGACY_VSCODE_VOLUME_PREFIX)] @@ -90,7 +90,7 @@ def test_failure_to_create_shared_volume_raises(monkeypatch): lambda self, name: name != manager.VSCODE_SERVER_VOLUME) with pytest.raises(manager.DockerError): - manager.ensure_volumes(1, project_name="proj-a") + manager.ensure_volumes(1) # ---------------------------------------------------------------------------