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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 52 additions & 20 deletions containers/base/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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/<commit>/vscode-server-linux-<arch>/ に残ったもの
# 3. 60 分より古い展開の途中のディレクトリ bin/<commit>_<数字>
#
# シンボリックリンクは辿らない。bin/ 自身・bin/<commit>・入れ子がリンクなら、版にも
# 片付けの対象にも数えない。リンク先の 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##*/}"
Expand All @@ -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/<commit>/vscode-server-linux-<arch>/
# に残ったもの。残した版の名前は標準入力から 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
Expand All @@ -701,6 +702,15 @@ devbase_prune_vscode_server_bin() {
done
done

return 0
}

# 60 分より古い展開の途中のディレクトリ bin/<commit>_<数字> を消す (#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
Expand All @@ -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/<commit>・入れ子がリンクなら、版にも
# 片付けの対象にも数えない。リンク先の 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 の設定ディレクトリ
# ===================================================================
Expand Down
4 changes: 2 additions & 2 deletions lib/devbase/commands/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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')
Expand Down
16 changes: 2 additions & 14 deletions lib/devbase/volume/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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

Expand All @@ -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)


Expand Down
15 changes: 9 additions & 6 deletions tests/commands/test_container_scale_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)))

Expand Down Expand Up @@ -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

Expand All @@ -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'

Expand Down
14 changes: 7 additions & 7 deletions tests/volume/test_manager_ensure_volumes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down Expand Up @@ -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"),
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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)
Expand All @@ -158,15 +158,15 @@ 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 == []


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",
Expand Down
14 changes: 7 additions & 7 deletions tests/volume/test_manager_vscode.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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

Expand All @@ -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)]
Expand All @@ -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)


# ---------------------------------------------------------------------------
Expand Down
Loading