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
121 changes: 49 additions & 72 deletions lib/devbase/commands/container.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,7 +25,8 @@
wait_for_containers_ready,
ensure_network
)
from devbase.utils.config import get_project_name, get_container_scale
from devbase.utils.config import get_project_name
from devbase.project import runtime as project_runtime

logger = get_logger(__name__)

Expand DownExpand Up@@ -85,13 +86,19 @@ def _inject_secrets(*, required: bool):
return _runtime.SecretEnv()


def _generate_compose_for(scale: int, secrets) -> Path:
"""機密の内訳を渡してスケール構成を生成する"""
def _generate_compose_for(scale: int, secrets, dev_environment=None) -> Path:
"""機密の内訳と devbase 由来の環境変数を渡してスケール構成を生成する。

``dev_environment`` は ``project.yml`` から作った clone プラン等
(:func:`devbase.project.runtime.container_env`)。dev サービスへ載せることで、
entrypoint がコンテナ内で複数リポジトリを clone できる。
"""
return generate_scaled_compose(
scale,
secret_env_names=secrets.names,
global_env_names=secrets.global_names,
project_env_names=secrets.project_names,
dev_environment=dev_environment,
)


Expand DownExpand Up@@ -276,18 +283,17 @@ def _load_project_env(env_file: Path) -> None:

wrapper (bin/devbase) は cd 後に ``source ./env`` で env を読み込むため、
Python フォールバック経路でも同じ KEY=VALUE を ``os.environ`` に載せて
変数欠落 (例: project 固有の ``CONTAINER_SCALE``) を防ぐ。
変数欠落 (例: project 固有の ``ENABLE_SSH``) を防ぐ。

env は環境変数定義のみを想定したファイル (bin/devbase 冒頭コメント参照) の
ため、ここでは ``export`` 接頭辞付き / 無しの単純な ``KEY=VALUE`` 行のみを
解釈する。``#`` コメント・空行は無視し、値の前後のクォートは除去する。

変数参照 (``$VAR`` / ``${VAR}``) は shell ``source ./env`` (wrapper 経路) と
同様に展開する。実 env が ``WORK_DIR=/work/$GIT_REPO`` のように同一ファイル内で
先に定義した変数を参照しており、展開しないと TUI (``list``) 経路でワークスペース
パスが ``$GIT_REPO`` 等の未展開文字列のまま VS Code で開いてしまうため
(行は file 順に ``os.environ`` へ載せるので、参照時には先行行の値が解決済み)。
単一引用符 ``'...'`` の値は shell 同様リテラル扱いで展開しない。
同様に展開する。``FOO=$BAR/baz`` のように同一ファイル内で先に定義した変数を
参照する書き方を wrapper 経路と揃えるため (行は file 順に ``os.environ`` へ
載せるので、参照時には先行行の値が解決済み)。単一引用符 ``'...'`` の値は
shell 同様リテラル扱いで展開しない。

.. note:: shell ``source`` との仕様乖離について

Expand DownExpand Up@@ -323,10 +329,9 @@ def _load_project_env(env_file: Path) -> None:
if len(value) >= 2 and value[0] == value[-1] and value[0] in ('"', "'"):
single_quoted = value[0] == "'"
value = value[1:-1]
# shell `source ./env` 相当の変数展開 ($VAR / ${VAR}) を行う。実 env は
# `WORK_DIR=/work/$GIT_REPO` のように同一ファイル内で先に定義した変数を
# 参照しており (行順に os.environ へ載せるため参照時には解決済み)、展開
# しないと TUI (list) 経路でワークスペースパスが未展開のまま開いてしまう。
# shell `source ./env` 相当の変数展開 ($VAR / ${VAR}) を行う。同一ファイル内で
# 先に定義した変数を参照する書き方 (`FOO=$BAR/baz`) を wrapper 経路と揃える
# ため (行順に os.environ へ載せるため参照時には解決済み)。
# 単一引用符はリテラル ($BAR を展開しない) という shell 規則に合わせ、
# `'...'` の場合のみ展開しない。展開は _expand_env_vars に委ね、`$VAR` /
# `${VAR}` のみ展開し (未定義は空文字 = shell source 準拠)、`\$` はリテラル
Expand DownExpand Up@@ -563,11 +568,15 @@ def _resolve_open_index(open_index: Optional[int], scale: int) -> int:

def _maybe_open_editor(project_name: str, open_flag: Optional[bool],
open_index: Optional[int], scale: int,
compose_file=None) -> None:
config, compose_file=None) -> None:
"""`up` 完了後に dev コンテナへ接続したエディタを開く ([6/6])。

有効判定は ``open_flag`` (CLI ``--open``/``--no-open``) が優先、None なら env
``DEVBASE_OPEN_EDITOR``。エディタ起動の成否は ``up`` の戻り値に影響させない。
有効判定は ``open_flag`` (CLI ``--open``/``--no-open``) が優先、None なら
``project.yml`` の ``open_editor``、それも無ければ env ``DEVBASE_OPEN_EDITOR``。
エディタ起動の成否は ``up`` の戻り値に影響させない。

開く対象は ``config`` (``project.yml``) から決める。repo が 1 件なら primary の
フォルダ、2 件以上なら entrypoint が書き出した ``*.code-workspace``。

``open_index`` は起動済みインスタンス範囲 ``1..scale`` 内である必要がある。
0・負数・``scale`` 超過は存在しないコンテナ URI になり原因不明な起動失敗を招くため、
Expand All@@ -579,7 +588,8 @@ def _maybe_open_editor(project_name: str, open_flag: Optional[bool],
"""
from devbase.editor import opener

enabled = open_flag if open_flag is not None else opener.is_open_enabled()
enabled = (open_flag if open_flag is not None
else opener.is_open_enabled(config=config))
if not enabled:
return

Expand All@@ -591,13 +601,18 @@ def _maybe_open_editor(project_name: str, open_flag: Optional[bool],
compose_file = _SCALE_COMPOSE_FILE

dev_service_name = get_dev_service_name()
workdir = opener.resolve_workdir(os.environ, project_name)
workdir = config.resolved_work_dir()
# repo が 2 件以上なら multi-root workspace を開く (entrypoint が同じパスへ
# ファイルを書き出している)。1 件なら従来どおりフォルダを開く。
workspace = (project_runtime.workspace_path(project_name)
if len(config.repos) > 1 else None)
logger.info("[6/6] Opening editor attached to the dev container...")
try:
opener.open_editor(
project_name=project_name,
dev_service_name=dev_service_name,
workdir=workdir,
workspace=workspace,
index=open_index,
compose_file=compose_file,
)
Expand All@@ -612,8 +627,11 @@ def cmd_up(project_name: str = None, scale: int = None,
if project_name is None:
project_name = get_project_name()

# project.yml が唯一の正 (PLAN32)。読めなければ移行手順を案内して止まる。
config = project_runtime.current_project_config()

if scale is None:
scale = get_container_scale()
scale = config.scale if config.scale is not None else project_runtime.DEFAULT_SCALE

dev_service_name = get_dev_service_name()

Expand DownExpand Up@@ -653,7 +671,9 @@ def cmd_up(project_name: str = None, scale: int = None,
# にしないため。
with _previous_scale_compose() as down_compose_file:
logger.info("[2/6] Generating scaled compose file...")
override_file = _generate_compose_for(scale, _inject_secrets(required=True))
override_file = _generate_compose_for(
scale, _inject_secrets(required=True),
dev_environment=project_runtime.container_env(config, project_name))
logger.info("Generated: %s", override_file)

logger.info("[3/6] Stopping existing containers...")
Expand All@@ -676,7 +696,7 @@ def cmd_up(project_name: str = None, scale: int = None,
_run_deploy_script_for_instances(deploy_script, range(1, scale + 1))

_maybe_open_editor(project_name, open_editor, open_index, scale,
compose_file=override_file)
config, compose_file=override_file)

logger.info("=== Deploy completed successfully ===")
return 0
Expand DownExpand Up@@ -763,8 +783,10 @@ def cmd_scale(new_scale: int, project_name: str = None) -> int:
if project_name is None:
project_name = get_project_name()

config = project_runtime.current_project_config()
dev_service_name = get_dev_service_name()
current_scale = _get_current_scale()
current_scale = (config.scale if config.scale is not None
else project_runtime.DEFAULT_SCALE)

logger.info("Scaling project '%s' from %d to %d containers (dev service: %s)",
project_name, current_scale, new_scale, dev_service_name)
Expand All@@ -779,9 +801,9 @@ def cmd_scale(new_scale: int, project_name: str = None) -> int:
return 1

try:
logger.info("[1/5] Updating env file: CONTAINER_SCALE=%d -> %d...", current_scale, new_scale)
if not _update_scale_in_env(new_scale):
return 1
logger.info("[1/5] Updating %s: scale=%d -> %d...",
project_runtime.PROJECT_CONFIG_FILENAME, current_scale, new_scale)
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)
Expand All@@ -791,7 +813,8 @@ def cmd_scale(new_scale: int, project_name: str = None) -> int:

logger.info("[3/5] Generating scaled compose file...")
override_file = _generate_compose_for(
new_scale, _inject_secrets(required=True))
new_scale, _inject_secrets(required=True),
dev_environment=project_runtime.container_env(config, project_name))
logger.info("Generated: %s", override_file)

logger.info("[4/5] Starting new containers (%d..%d)...", current_scale + 1, new_scale)
Expand DownExpand Up@@ -1392,49 +1415,3 @@ def _mark_pulled(image_name: str) -> None:
marker.touch()
except OSError as e:
logger.warning("Could not write pull marker for '%s': %s", image_name, e)


def _update_scale_in_env(new_scale: int) -> bool:
"""Update CONTAINER_SCALE value in env file"""
env_file = Path('./env')

if not env_file.exists():
logger.error("env file not found: %s", env_file)
return False

def _is_scale_line(line: str) -> bool:
return line.strip().startswith('CONTAINER_SCALE=')

try:
lines = env_file.read_text().splitlines(keepends=True)
new_lines = [
f'CONTAINER_SCALE={new_scale}\n' if _is_scale_line(line) else line
for line in lines
]
if not any(map(_is_scale_line, lines)):
new_lines.append(f'\n# Added by devbase scale command\nCONTAINER_SCALE={new_scale}\n')
env_file.write_text(''.join(new_lines))
return True

except Exception as e:
logger.error("Updating env file: %s", e)
return False


def _get_current_scale() -> int:
"""Get current CONTAINER_SCALE from env file"""
env_file = Path('./env')

if not env_file.exists():
return 0

try:
with open(env_file, 'r') as f:
for line in f:
if line.strip().startswith('CONTAINER_SCALE='):
value = line.split('=', 1)[1].strip()
return int(value)
except Exception:
pass

return 0
35 changes: 19 additions & 16 deletions lib/devbase/editor/opener.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -253,8 +253,15 @@ def detect_context(environ=None, isatty: Optional[bool] = None,
)


def is_open_enabled(environ=None) -> bool:
"""``DEVBASE_OPEN_EDITOR`` env が真かどうか (未設定は False)。"""
def is_open_enabled(environ=None, config=None) -> bool:
"""エディタを自動で開くかどうか。

プロジェクト設定 (``project.yml`` の ``open_editor``) が指定されていればそれを
採る。未指定なら env ``DEVBASE_OPEN_EDITOR`` (グローバル ``.env`` の既定値)
を見る。どちらも無ければ開かない。
"""
if config is not None and config.open_editor is not None:
return config.open_editor
env = os.environ if environ is None else environ
value = env.get("DEVBASE_OPEN_EDITOR")
if value is None:
Expand DownExpand Up@@ -425,23 +432,17 @@ def resolve_container_name(dev_service_name: str, project_name: str, index: int
return f"{project_name}-{dev_service_name}-{index}"


def resolve_workdir(environ=None, project_name: Optional[str] = None) -> str:
"""コンテナ内で開くワークスペースパス (``/work/$GIT_REPO``) を返す。"""
env = os.environ if environ is None else environ
workdir = env.get("WORK_DIR")
if workdir:
return workdir
repo = env.get("GIT_REPO") or project_name
return f"/work/{repo}" if repo else "/work"


def resolve_workspace(environ=None) -> Optional[str]:
"""開く VS Code ワークスペースファイル (``*.code-workspace``) のコンテナ内パス。

``DEVBASE_WORKSPACE`` env にコンテナ内の絶対パス (例
``/home/ubuntu/share/work/uttarov2-doc.workspace``) が指定されていればそれを返す。
未設定・空文字なら None を返し、呼び出し側 (:func:`open_editor`) は従来どおり
:func:`resolve_workdir` のフォルダを ``--folder-uri`` で開く。
未設定・空文字なら None を返し、呼び出し側 (:func:`open_editor`) はフォルダを
``--folder-uri`` で開く。

複数リポジトリのプロジェクトでは ``devbase up`` が ``project.yml`` から
workspace パスを決めて :func:`open_editor` の ``workspace`` 引数で直接渡す。
この env はそれを手動で上書きしたい場合の口として残している。

ワークスペースファイルはコンテナ内に実在するパスを指す前提 (attach 先は
コンテナ authority のため)。``/home/ubuntu/share`` 等の共有マウント配下に置けば
Expand DownExpand Up@@ -622,6 +623,7 @@ def _launch(cmd: list, env: dict) -> None:


def open_editor(*, project_name: str, dev_service_name: str, workdir: str,
workspace: Optional[str] = None,
index: int = 1, compose_file=None,
environ=None,
isatty: Optional[bool] = None, system: Optional[str] = None,
Expand All@@ -633,7 +635,8 @@ def open_editor(*, project_name: str, dev_service_name: str, workdir: str,
握り潰して warning にし、``up`` 本体を絶対に失敗させない。``isatty`` /
``system`` / ``ipc_alive`` は :func:`detect_context` への差し替え口 (テスト用)。
``compose_file`` は実コンテナ名問い合わせ時に起動と同じ override compose を
``-f`` で渡すため。
``-f`` で渡すため。``workspace`` は複数リポジトリ構成で開く
``*.code-workspace`` のコンテナ内パス (未指定なら env ``DEVBASE_WORKSPACE``)。
"""
env = os.environ if environ is None else environ
ctx = detect_context(env, isatty=isatty, system=system, ipc_alive=ipc_alive)
Expand DownExpand Up@@ -681,7 +684,7 @@ def open_editor(*, project_name: str, dev_service_name: str, workdir: str,
# DEVBASE_WORKSPACE があれば *.code-workspace をワークスペースとして開く。VS Code は
# `--file-uri` に渡したパスが .code-workspace 拡張子なら multi-root ワークスペースとして
# 開くため、フォルダを開く `--folder-uri` と URI ターゲット・フラグの両方を切り替える。
workspace = resolve_workspace(env)
workspace = workspace or resolve_workspace(env)
open_target = workspace or workdir
uri_flag = "--file-uri" if workspace else "--folder-uri"
uri = build_attach_uri(container, open_target,
Expand Down
4 changes: 2 additions & 2 deletions lib/devbase/env/runtime.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -115,8 +115,8 @@ def _project_env_overrides(devbase_root: Path, project: str) -> Dict[str, str]:
"""プロジェクトの非機密設定 (``projects/<name>/env``) による上書き値。

値そのものはファイルから読まず、既に環境変数へ載っているものだけを採用する。
``env`` は ``WORK_DIR=/work/$GIT_REPO`` のように同一ファイル内の変数を参照
するため、起動ラッパー (または ``_load_project_env``) が展開した後の値が
``env`` は ``APP_ROOT=$APP_HOME/app`` のように同一ファイル内の変数を参照
できるため、起動ラッパー (または ``_load_project_env``) が展開した後の値が
正しく、ここで生の行を読み直すと未展開の文字列を掴んでしまう。
"""
path = Path(devbase_root) / 'projects' / project / 'env'
Expand Down
Loading