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
7 changes: 7 additions & 0 deletions lib/devbase/volume/compose.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,9 @@ def generate_scaled_compose(
if service_name != dev_service_name:
copied = _deep_copy(service_config)
_rewrite_depends_on(copied, dev_service_name, scale)
# Insert tini as PID 1 so orphaned children are reaped (no zombies).
# setdefault keeps an explicit `init: false` if the project set one.
copied.setdefault('init', True)
scaled_config['services'][service_name] = copied

# Generate a service for each instance
Expand All @@ -216,6 +219,10 @@ def generate_scaled_compose(
# Update container name
service['container_name'] = f"${{COMPOSE_PROJECT_NAME}}-{dev_service_name}-{i}"

# Insert tini as PID 1 so orphaned children are reaped (no zombies).
# setdefault keeps an explicit `init: false` if the project set one.
service.setdefault('init', True)

# Remove environment section (use env_file instead to avoid exposing secrets)
if 'environment' in service:
del service['environment']
Expand Down
Empty file added tests/volume/__init__.py
Empty file.
74 changes: 74 additions & 0 deletions tests/volume/test_compose.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""compose.py: generate_scaled_compose() の `init: true` 注入挙動 (issue #28)

devbase コンテナの PID 1 は entrypoint の `tail -f /dev/null` であり orphan を reap しない。
docker の `init: true` で tini を PID 1 に挿入しゾンビ蓄積を防ぐため、生成される全サービスに
`init` を注入する。ユーザーが明示した `init: false` は setdefault で尊重する。
"""

from __future__ import annotations

import os

import yaml
import pytest

from devbase.volume import compose


@pytest.fixture
def in_tmp_cwd(tmp_path, monkeypatch):
"""生成物 (.docker-compose.scale.yml) が散らからないよう CWD を tmp に移す。"""
monkeypatch.chdir(tmp_path)
# DEV_SERVICE_NAME が外部環境に左右されないよう既定に固定
monkeypatch.delenv("DEV_SERVICE_NAME", raising=False)
return tmp_path


def _write_compose(tmp_path, services: dict) -> None:
(tmp_path / "compose.yml").write_text(
yaml.safe_dump({"services": services}, sort_keys=False),
encoding="utf-8",
)


def _load_scaled(tmp_path) -> dict:
return yaml.safe_load((tmp_path / ".docker-compose.scale.yml").read_text())


def test_dev_and_non_dev_services_get_init_true(in_tmp_cwd):
"""dev インスタンスと non-dev サービスの双方に init: true が注入される。"""
_write_compose(in_tmp_cwd, {
"dev": {"image": "dev:latest"},
"mysql": {"image": "mysql:8"},
})

compose.generate_scaled_compose(scale=1, project_name="proj")
scaled = _load_scaled(in_tmp_cwd)["services"]

assert scaled["dev-1"]["init"] is True
assert scaled["mysql"]["init"] is True


def test_init_injected_for_every_scaled_instance(in_tmp_cwd):
"""scale>1 でも各 dev-i 全てに init: true が付く。"""
_write_compose(in_tmp_cwd, {"dev": {"image": "dev:latest"}})

compose.generate_scaled_compose(scale=3, project_name="proj")
scaled = _load_scaled(in_tmp_cwd)["services"]

for i in (1, 2, 3):
assert scaled[f"dev-{i}"]["init"] is True


def test_explicit_init_false_is_preserved(in_tmp_cwd):
"""明示的な init: false は setdefault により上書きされない。"""
_write_compose(in_tmp_cwd, {
"dev": {"image": "dev:latest", "init": False},
"mysql": {"image": "mysql:8", "init": False},
})

compose.generate_scaled_compose(scale=1, project_name="proj")
scaled = _load_scaled(in_tmp_cwd)["services"]

assert scaled["dev-1"]["init"] is False
assert scaled["mysql"]["init"] is False
Loading