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
607 changes: 333 additions & 274 deletions etc/devbase-completion.bash

Large diffs are not rendered by default.

22 changes: 13 additions & 9 deletions lib/devbase/commands/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -875,25 +875,31 @@ def _aws_sync_plan(ctx: _SyncContext):
プロファイルの集合が値のプロファイルの集合と一致すれば丸ごと。値が読めない、または値にあって
今のファイルに無いプロファイルがあれば書かない (決定 10)
"""
from devbase.env.collectors.aws import _encode_aws_config_files
from devbase.env.host_import import ALL

source = ctx.sources.get_source('aws')
if source and source.get('type') == 'aws_profiles':
return _aws_plan_for_selection(ctx, list(source.get('profiles') or []))
if source:
return _encode_aws_config_files, ALL
return _aws_whole_plan()
return _aws_plan_for_unregistered(ctx)


def _aws_whole_plan():
"""``~/.aws`` を丸ごと入れ直す計画 ``(encode_fn, ALL)`` (I8)"""
from devbase.env.collectors.aws import _encode_aws_config_files
from devbase.env.host_import import ALL

return _encode_aws_config_files, ALL


def _aws_plan_for_selection(ctx: _SyncContext, profiles):
"""控えが ``aws_profiles`` のとき、選んだプロファイルとその連なりだけを切り出す計画 (I7)"""
from devbase.env import aws_profiles

plan = (lambda: aws_profiles.build_from_home(profiles).encode()), profiles
key = keys.AWS_CONFIG_BASE64
targets = ctx.targets
if targets.holder(key) is None:
return (lambda: aws_profiles.build_from_home(profiles).encode()), profiles
return plan
payload = aws_profiles.build_from_home(profiles)
_report_unknown_profiles(payload)
if _report_profile_conflicts(payload):
Expand All @@ -905,14 +911,13 @@ def _aws_plan_for_selection(ctx: _SyncContext, profiles):
"(選択はグループの控えのもの。入れ直すなら devbase env init --reset)",
_AWS_LABEL, ctx.store.display_label(dest.ref))
return None
return (lambda: aws_profiles.build_from_home(profiles).encode()), profiles
return plan


def _aws_plan_for_unregistered(ctx: _SyncContext):
"""控えに項目が無いとき、参照の値のプロファイルを選択とみなす計画 (決定 10)"""
from devbase.env import aws_profiles
from devbase.env.collectors.aws import _encode_aws_config_files
from devbase.env.host_import import ALL

key = keys.AWS_CONFIG_BASE64
targets = ctx.targets
Expand All @@ -931,7 +936,7 @@ def _aws_plan_for_unregistered(ctx: _SyncContext):
current = (set(aws_profiles.profile_names(config_text))
| set(aws_profiles.credential_names(credentials_text)))
if current == set(in_value):
return _encode_aws_config_files, ALL
return _aws_whole_plan()
payload = aws_profiles.build(config_text, credentials_text, in_value)
if payload.unknown:
_report_unknown_profiles(payload)
Expand Down Expand Up @@ -1687,7 +1692,6 @@ def cmd_env_keygen(devbase_root: Path, force: bool = False,
生成先と探索先が構造的に一致する契約を保つ。
"""
from devbase.env import agekeys
from devbase.errors import DevbaseError

path = agekeys.key_file_path()

Expand Down
3 changes: 2 additions & 1 deletion lib/devbase/volume/compose.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

from . import bind_mounts
from .manager import (
CONTAINER_HOME,
VSCODE_BIN_TARGET,
VSCODE_SERVER_TARGET,
VSCODE_SERVER_VOLUME,
Expand All @@ -27,7 +28,7 @@
logger = get_logger(__name__)

# 旧 /home/ubuntu マウントは非推奨のため scale 生成時に除去する
_DEPRECATED_TARGET = '/home/ubuntu'
_DEPRECATED_TARGET = CONTAINER_HOME

# VS Code Server の置き場 (PLAN36 / #463)。本体 (bin/) だけを全コンテナ共通の
# named volume に置く。コンテナの書き込みレイヤに置くと devbase up のたびに
Expand Down
6 changes: 4 additions & 2 deletions lib/devbase/volume/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
WORK_VOLUME_PREFIX = "devbase_work_"
# 全コンテナで共有するホームディレクトリボリューム
HOME_UBUNTU_VOLUME = "devbase_home_ubuntu"
#: コンテナの中の利用者 ubuntu のホーム
CONTAINER_HOME = '/home/ubuntu'

# --- アカウントグループ (PLAN39 / #315) --------------------------------------
# アカウントグループは「使用する Google / AWS アカウントの単位」。グループごとに
Expand Down Expand Up @@ -46,9 +48,9 @@
VSCODE_SERVER_VOLUME = "devbase_vscode-server-bin"
#: VS Code Server の置き場。生成する compose のマウント先と、起動の後の処理が
#: 所有者を直す先の両方がこれを使う
VSCODE_SERVER_TARGET = '/home/ubuntu/.vscode-server'
VSCODE_SERVER_TARGET = f'{CONTAINER_HOME}/.vscode-server'
#: 本体の置き場。:data:`VSCODE_SERVER_VOLUME` を付ける先
VSCODE_BIN_TARGET = '/home/ubuntu/.vscode-server/bin'
VSCODE_BIN_TARGET = f'{VSCODE_SERVER_TARGET}/bin'
#: #463 より前の、~/.vscode-server を丸ごと持つコンテナごとのボリュームの接頭辞
LEGACY_VSCODE_VOLUME_PREFIX = "devbase_vscode_"
_LEGACY_VSCODE_VOLUME_RE = re.compile(r"^devbase_vscode_.+_[0-9]+$")
Expand Down
26 changes: 26 additions & 0 deletions tests/cli/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

REPO_ROOT = Path(__file__).resolve().parents[2]
WRAPPER = REPO_ROOT / "bin" / "devbase"
BASH_COMPLETION = REPO_ROOT / "etc" / "devbase-completion.bash"

_FAKE_UV = """\
#!/bin/bash
Expand Down Expand Up @@ -213,3 +214,28 @@ def compose_wrapper(exec_wrapper) -> ComposeWrapperRoot:
wrapper = ComposeWrapperRoot(root)
wrapper.extra_env = {}
return wrapper


def run_bash_completion(comp_words: str, cword: int, devbase_root) -> subprocess.CompletedProcess:
"""bash 補完を source して ``_devbase_completions`` を実行し、COMPREPLY を改行区切りで出す。

``comp_words`` は ``COMP_WORDS=(...)`` の中へそのまま入れる語の並び (引用符を含む)。
``DEVBASE_ROOT`` は ``devbase_root`` を渡し、実環境の値を継承しない。
"""
script = f"""
set -e
source "{BASH_COMPLETION}"
COMP_WORDS=({comp_words})
COMP_CWORD={cword}
_devbase_completions
printf '%s\\n' "${{COMPREPLY[@]}}"
"""
env = {**os.environ, "DEVBASE_ROOT": str(devbase_root)}
return subprocess.run(["bash", "-c", script], capture_output=True, text=True, env=env, check=False)


def bash_complete(comp_words: str, cword: int, devbase_root) -> list[str]:
"""bash 補完を source して COMPREPLY を改行区切りで返す。"""
proc = run_bash_completion(comp_words, cword, devbase_root)
assert proc.returncode == 0, proc.stderr
return [line for line in proc.stdout.splitlines() if line]
148 changes: 131 additions & 17 deletions tests/cli/test_completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,34 +8,19 @@

from __future__ import annotations

import os
import shutil
import subprocess
from pathlib import Path

import pytest

from tests.cli.conftest import bash_complete as _bash_complete

REPO_ROOT = Path(__file__).resolve().parents[2]
BASH_COMPLETION = REPO_ROOT / "etc" / "devbase-completion.bash"
ZSH_COMPLETION = REPO_ROOT / "etc" / "_devbase"


def _bash_complete(words, cword, devbase_root):
"""bash 補完を source して COMPREPLY を改行区切りで返す。"""
script = f"""
set -e
source "{BASH_COMPLETION}"
COMP_WORDS=({words})
COMP_CWORD={cword}
_devbase_completions
printf '%s\\n' "${{COMPREPLY[@]}}"
"""
env = {**os.environ, "DEVBASE_ROOT": str(devbase_root)}
proc = subprocess.run(["bash", "-c", script], capture_output=True, text=True, env=env)
assert proc.returncode == 0, proc.stderr
return [line for line in proc.stdout.splitlines() if line]


@pytest.fixture
def fake_root(tmp_path):
projects = tmp_path / "projects"
Expand Down Expand Up @@ -281,3 +266,132 @@ def test_bash_container_flags_only_without_names(fake_root, words, cword, expect
])
def test_bash_context_value_is_not_completed(fake_root, words, cword):
assert _bash_complete(words, cword, fake_root) == []


# #452: bash の補完の各分岐の候補の現状固定(R9・R10 で分岐を関数へ移す前に縛る)
_PLUGINS = {"alpha", "beta"}
_REPOS = ["official", "extra"]
_ENV_EXPORT_FLAGS = ["--include-project", "--exclude-project", "--no-global", "--no-metadata",
"--recipient", "--passphrase-env", "--passphrase-stdin", "--force-unencrypted",
"--unsafe-allow-unencrypted-bucket"]
_ENV_IMPORT_FLAGS = ["--merge", "--replace-keys", "--replace", "--dry-run", "--identity",
"--passphrase-env", "--passphrase-stdin", "--include-project", "--exclude-project",
"--no-global", "--no-metadata", "--merge-metadata", "--backup-dir", "--keep-last"]
_LIST_FLAGS = ["--no-interactive", "--plain", "-P", "--interactive", "-i"]
_CONTAINER_SUBCOMMANDS = ["up", "down", "ps", "login", "logs", "scale", "build", "rebuild", "profile", "open"]
_ENV_SUBCOMMANDS = ["init", "sync", "list", "set", "get", "delete", "edit", "project", "export", "import",
"keygen", "exec", "token", "encrypt", "decrypt", "rekey", "doctor", "backend"]
_PLUGIN_SUBCOMMANDS = ["list", "install", "uninstall", "update", "info", "sync", "repo"]
_SNAPSHOT_SUBCOMMANDS = ["create", "list", "restore", "copy", "delete", "rotate"]


@pytest.fixture
def fake_root_with_plugins(fake_root):
"""プラグインの置き場(ディレクトリと symlink)と plugins.yml を持つ root。"""
plugins = fake_root / "plugins"
plugins.mkdir()
(plugins / "alpha").mkdir()
(fake_root / "beta_target").mkdir()
(plugins / "beta").symlink_to(fake_root / "beta_target")
(fake_root / "plugins.yml").write_text(
"repositories:\n - name: official\n - name: extra\n")
return fake_root


@pytest.mark.parametrize("words, cword, expected", [
# 位置 2: 1 語目ごと
("devbase login ''", 2, ["1", "2"]),
("devbase login '-'", 2, ["--context"]),
("devbase build ''", 2, []),
("devbase build '-'", 2, ["--context"]),
("devbase scale '-'", 2, ["--context"]),
("devbase rebuild ''", 2, _NAMES),
("devbase list ''", 2, _LIST_FLAGS),
("devbase list '-'", 2, _LIST_FLAGS),
("devbase container ''", 2, _CONTAINER_SUBCOMMANDS),
("devbase ct ''", 2, _CONTAINER_SUBCOMMANDS),
("devbase env ''", 2, _ENV_SUBCOMMANDS),
("devbase plugin ''", 2, _PLUGIN_SUBCOMMANDS),
("devbase pl ''", 2, _PLUGIN_SUBCOMMANDS),
("devbase snapshot ''", 2, _SNAPSHOT_SUBCOMMANDS),
("devbase ss ''", 2, _SNAPSHOT_SUBCOMMANDS),
("devbase status ''", 2, []),
# 位置 3: トップレベルのシノニム
("devbase open web '-'", 3, ["--open-index", "--context"]),
("devbase up web '-'", 3, ["--context"]),
("devbase scale web ''", 3, []),
# 位置 3: project
("devbase project login ''", 3, ["1", "2"]),
("devbase project login '-'", 3, ["--context"]),
("devbase project build '-'", 3, ["--context"]),
("devbase project build ''", 3, []),
("devbase project rebuild '-'", 3, ["--context"]),
("devbase project profile ''", 3, ["up", "down", "list"]),
# 位置 3: container / ct
("devbase container up '-'", 3, ["--context"]),
("devbase container up ''", 3, []),
("devbase ct build '-'", 3, ["--context"]),
("devbase container login ''", 3, ["1", "2"]),
("devbase container scale ''", 3, ["1", "2", "3", "4", "5"]),
("devbase ct scale '-'", 3, ["--context"]),
# 位置 3: env
("devbase env exec '-'", 3, ["--context"]),
("devbase env token '-'", 3, ["--context"]),
("devbase env init '-'", 3, ["--reset"]),
("devbase env init ''", 3, []),
("devbase env list '-'", 3, ["--global", "-g", "--project", "-p", "--reveal", "-r", "--keys", "-k"]),
("devbase env set '-'", 3, ["--project", "-p"]),
("devbase env delete '-'", 3, ["--project", "-p"]),
("devbase env edit '-'", 3, ["--project", "-p"]),
("devbase env export '-'", 3, _ENV_EXPORT_FLAGS),
("devbase env import '-'", 3, _ENV_IMPORT_FLAGS),
("devbase env sync '-'", 3, []),
# 位置 3: plugin / pl
("devbase plugin list ''", 3, ["--available"]),
("devbase pl install '-'", 3, ["--link", "--all"]),
("devbase plugin install ''", 3, []),
("devbase plugin uninstall ''", 3, _PLUGINS),
("devbase pl update ''", 3, _PLUGINS),
("devbase plugin info ''", 3, _PLUGINS),
("devbase plugin repo ''", 3, ["add", "remove", "list", "refresh"]),
# 位置 3: snapshot / ss
("devbase snapshot create '-'", 3, ["--name", "--full"]),
("devbase ss restore '-'", 3, ["--point"]),
("devbase snapshot rotate '-'", 3, ["--keep"]),
("devbase snapshot create ''", 3, []),
# 位置 4
("devbase project open web '-'", 4, ["--open-index", "--context"]),
("devbase project up web '-'", 4, ["--context"]),
("devbase project down web '-'", 4, ["--context"]),
("devbase project rebuild web '-'", 4, ["--context"]),
("devbase project scale web '-'", 4, ["--context"]),
("devbase project post-start web '-'", 4, ["--context"]),
("devbase project up web ''", 4, []),
("devbase project profile up '-'", 4, ["--context"]),
("devbase container profile down '-'", 4, ["--context"]),
("devbase ct profile list '-'", 4, ["--context"]),
("devbase container ps web '-'", 4, []),
("devbase plugin install src '-'", 4, ["--link", "--all"]),
("devbase pl install src ''", 4, []),
("devbase plugin repo add '-'", 4, ["--name"]),
("devbase env set KEY '-'", 4, []),
])
def test_bash_completion_branches(fake_root_with_plugins, words, cword, expected):
out = _bash_complete(words, cword, fake_root_with_plugins)
if isinstance(expected, set):
assert set(out) == expected
else:
assert out == expected


def _python3_has_yaml():
python3 = shutil.which("python3")
if python3 is None:
return False
return subprocess.run([python3, "-c", "import yaml"], capture_output=True).returncode == 0


@pytest.mark.skipif(not _python3_has_yaml(), reason="python3 か yaml が無い(リポジトリ名は python3 で plugins.yml を読む)")
@pytest.mark.parametrize("words", ["devbase pl repo remove ''", "devbase plugin repo refresh ''"])
def test_bash_completion_plugin_repo_names(fake_root_with_plugins, words):
assert _bash_complete(words, 4, fake_root_with_plugins) == _REPOS
15 changes: 2 additions & 13 deletions tests/cli/test_name_context_consistency.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,16 @@
from __future__ import annotations

import argparse
import os
import re
import subprocess
from pathlib import Path

import pytest

from devbase import cli
from tests.cli.conftest import run_bash_completion

REPO_ROOT = Path(__file__).resolve().parents[2]
WRAPPER = REPO_ROOT / "bin" / "devbase"
BASH_COMPLETION = REPO_ROOT / "etc" / "devbase-completion.bash"
ZSH_COMPLETION = REPO_ROOT / "etc" / "_devbase"
SPEC = REPO_ROOT / "docs" / "specifications" / "cli-argument-resolution.md"

Expand Down Expand Up @@ -178,16 +176,7 @@ def fake_root(tmp_path):
def _bash_complete(words, devbase_root, cur=""):
"""`devbase <words...> <cur>` の最後の位置で bash 補完を実行し、候補を返す。"""
quoted = " ".join(f'"{w}"' for w in ("devbase",) + tuple(words) + (cur,))
script = f"""
set -e
source "{BASH_COMPLETION}"
COMP_WORDS=({quoted})
COMP_CWORD={len(words) + 1}
_devbase_completions
printf '%s\\n' "${{COMPREPLY[@]}}"
"""
env = {**os.environ, "DEVBASE_ROOT": str(devbase_root)}
proc = subprocess.run(["bash", "-c", script], capture_output=True, text=True, env=env, check=False)
proc = run_bash_completion(quoted, len(words) + 1, devbase_root)
assert proc.returncode == 0, f"etc/devbase-completion.bash の実行に失敗: {words} {proc.stderr}"
return {line for line in proc.stdout.splitlines() if line}

Expand Down
19 changes: 1 addition & 18 deletions tests/cli/test_post_start_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,16 @@

from __future__ import annotations

import os
import subprocess
import sys
from pathlib import Path

import pytest

from devbase import cli
from tests.cli.conftest import bash_complete as _bash_complete
from tests.cli.conftest import python_args, stdout_field

REPO_ROOT = Path(__file__).resolve().parents[2]
BASH_COMPLETION = REPO_ROOT / "etc" / "devbase-completion.bash"
ZSH_COMPLETION = REPO_ROOT / "etc" / "_devbase"


Expand Down Expand Up @@ -78,21 +76,6 @@ def test_wrapper_project_post_start_name_cds_and_strips(exec_wrapper):
assert python_args(r) == "project post-start", r.stdout


def _bash_complete(words, cword, devbase_root):
script = f"""
set -e
source "{BASH_COMPLETION}"
COMP_WORDS=({words})
COMP_CWORD={cword}
_devbase_completions
printf '%s\\n' "${{COMPREPLY[@]}}"
"""
env = {**os.environ, "DEVBASE_ROOT": str(devbase_root)}
proc = subprocess.run(["bash", "-c", script], capture_output=True, text=True, env=env)
assert proc.returncode == 0, proc.stderr
return [line for line in proc.stdout.splitlines() if line]


def test_bash_completion_lists_post_start_only_under_project(tmp_path):
(tmp_path / "projects" / "web").mkdir(parents=True)

Expand Down
Loading
Loading