diff --git a/tests/cli/test_cli_clean.py b/tests/cli/test_cli_clean.py new file mode 100644 index 000000000..e467cd96f --- /dev/null +++ b/tests/cli/test_cli_clean.py @@ -0,0 +1,141 @@ +# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import Any + +from click.testing import CliRunner + +from veadk.cli.cli_clean import clean + + +class _FakeVeFaaS: + calls: list[dict[str, Any]] = [] + + def __init__(self, **kwargs: Any) -> None: + self.calls.append({"init": kwargs, "deleted": []}) + + def find_app_id_by_name(self, name: str) -> str | None: + self.calls[-1].setdefault("lookups", []).append(name) + if len(self.calls[-1]["lookups"]) == 1: + return "app-123" + return None + + def delete(self, app_id: str | None) -> None: + self.calls[-1]["deleted"].append(app_id) + + +def test_clean_defaults_to_volcengine_environment( + monkeypatch, +) -> None: + monkeypatch.delenv("AGENTKIT_CLOUD_PROVIDER", raising=False) + monkeypatch.delenv("CLOUD_PROVIDER", raising=False) + monkeypatch.setenv("VOLCENGINE_ACCESS_KEY", "volc-ak") + monkeypatch.setenv("VOLCENGINE_SECRET_KEY", "volc-sk") + monkeypatch.setenv("VOLCENGINE_SESSION_TOKEN", "volc-token") + monkeypatch.setenv("REGION", "cn-shanghai") + monkeypatch.setattr("veadk.integrations.ve_faas.ve_faas.VeFaaS", _FakeVeFaaS) + _FakeVeFaaS.calls.clear() + + result = CliRunner().invoke( + clean, + ["--vefaas-app-name", "studio-app"], + input="y\n", + ) + + assert result.exit_code == 0, result.output + assert _FakeVeFaaS.calls == [ + { + "init": { + "access_key": "volc-ak", + "secret_key": "volc-sk", + "session_token": "volc-token", + "region": "cn-shanghai", + "provider": "volcengine", + }, + "deleted": ["app-123"], + "lookups": ["studio-app", "studio-app"], + } + ] + + +def test_clean_uses_byteplus_environment( + monkeypatch, +) -> None: + monkeypatch.setenv("CLOUD_PROVIDER", "byteplus") + monkeypatch.delenv("AGENTKIT_CLOUD_PROVIDER", raising=False) + monkeypatch.setenv("BYTEPLUS_ACCESS_KEY", "byteplus-ak") + monkeypatch.setenv("BYTEPLUS_SECRET_KEY", "byteplus-sk") + monkeypatch.setenv("BYTEPLUS_SESSION_TOKEN", "byteplus-token") + monkeypatch.setenv("BYTEPLUS_REGION", "ap-southeast-1") + monkeypatch.setenv("VOLCENGINE_ACCESS_KEY", "volc-ak") + monkeypatch.setenv("VOLCENGINE_SECRET_KEY", "volc-sk") + monkeypatch.setattr("veadk.integrations.ve_faas.ve_faas.VeFaaS", _FakeVeFaaS) + _FakeVeFaaS.calls.clear() + + result = CliRunner().invoke( + clean, + ["--vefaas-app-name", "studio-app"], + input="y\n", + ) + + assert result.exit_code == 0, result.output + assert _FakeVeFaaS.calls[0]["init"] == { + "access_key": "byteplus-ak", + "secret_key": "byteplus-sk", + "session_token": "byteplus-token", + "region": "ap-southeast-1", + "provider": "byteplus", + } + assert _FakeVeFaaS.calls[0]["deleted"] == ["app-123"] + + +def test_clean_explicit_byteplus_options_override_environment( + monkeypatch, +) -> None: + monkeypatch.setenv("CLOUD_PROVIDER", "volcengine") + monkeypatch.setenv("BYTEPLUS_ACCESS_KEY", "env-ak") + monkeypatch.setenv("BYTEPLUS_SECRET_KEY", "env-sk") + monkeypatch.setenv("BYTEPLUS_SESSION_TOKEN", "env-token") + monkeypatch.setattr("veadk.integrations.ve_faas.ve_faas.VeFaaS", _FakeVeFaaS) + _FakeVeFaaS.calls.clear() + + result = CliRunner().invoke( + clean, + [ + "--provider", + "byteplus", + "--region", + "ap-southeast-1", + "--byteplus-access-key", + "cli-ak", + "--byteplus-secret-key", + "cli-sk", + "--byteplus-session-token", + "cli-token", + "--vefaas-app-name", + "studio-app", + ], + input="y\n", + ) + + assert result.exit_code == 0, result.output + assert _FakeVeFaaS.calls[0]["init"] == { + "access_key": "cli-ak", + "secret_key": "cli-sk", + "session_token": "cli-token", + "region": "ap-southeast-1", + "provider": "byteplus", + } diff --git a/tests/cli/test_cli_deploy.py b/tests/cli/test_cli_deploy.py new file mode 100644 index 000000000..82a1bb480 --- /dev/null +++ b/tests/cli/test_cli_deploy.py @@ -0,0 +1,111 @@ +# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import os +from pathlib import Path +from types import SimpleNamespace +from typing import Any + +from click.testing import CliRunner + +from veadk.cli.cli_deploy import deploy +from veadk.config import veadk_environments + + +def _write_agent_project(path: Path) -> None: + path.mkdir() + (path / "__init__.py").write_text("from . import agent\n", encoding="utf-8") + (path / "agent.py").write_text("root_agent = object()\n", encoding="utf-8") + + +def test_deploy_reads_byteplus_provider_environment( + monkeypatch, + tmp_path: Path, +) -> None: + project = tmp_path / "agent-proj" + _write_agent_project(project) + captured: dict[str, Any] = {} + + def fake_cookiecutter( + template: str, + output_dir: str, + no_input: bool, + extra_context: dict[str, Any], + ) -> None: + captured["template"] = template + captured["no_input"] = no_input + captured["extra_context"] = extra_context + generated_agent_dir = ( + Path(output_dir) / extra_context["local_dir_name"] / "src" / "agent_proj" + ) + generated_agent_dir.mkdir(parents=True) + + async def fake_main() -> None: + captured["main_env"] = { + "CLOUD_PROVIDER": os.environ.get("CLOUD_PROVIDER"), + "AGENTKIT_CLOUD_PROVIDER": os.environ.get("AGENTKIT_CLOUD_PROVIDER"), + "BYTEPLUS_REGION": os.environ.get("BYTEPLUS_REGION"), + "REGION": os.environ.get("REGION"), + "VOLCENGINE_ACCESS_KEY": os.environ.get("VOLCENGINE_ACCESS_KEY"), + "VOLCENGINE_SECRET_KEY": os.environ.get("VOLCENGINE_SECRET_KEY"), + "VOLCENGINE_SESSION_TOKEN": os.environ.get("VOLCENGINE_SESSION_TOKEN"), + } + + monkeypatch.setenv("CLOUD_PROVIDER", "byteplus") + monkeypatch.delenv("AGENTKIT_CLOUD_PROVIDER", raising=False) + monkeypatch.delenv("REGION", raising=False) + monkeypatch.delenv("VOLCENGINE_ACCESS_KEY", raising=False) + monkeypatch.delenv("VOLCENGINE_SECRET_KEY", raising=False) + monkeypatch.delenv("VOLCENGINE_SESSION_TOKEN", raising=False) + monkeypatch.setenv("BYTEPLUS_REGION", "ap-southeast-1") + monkeypatch.setenv("BYTEPLUS_ACCESS_KEY", "byteplus-ak") + monkeypatch.setenv("BYTEPLUS_SECRET_KEY", "byteplus-sk") + monkeypatch.setenv("BYTEPLUS_SESSION_TOKEN", "byteplus-token") + for key in ( + "CLOUD_PROVIDER", + "AGENTKIT_CLOUD_PROVIDER", + "BYTEPLUS_REGION", + ): + monkeypatch.delitem(veadk_environments, key, raising=False) + monkeypatch.setattr("veadk.utils.misc.formatted_timestamp", lambda: "20260824") + monkeypatch.setattr("cookiecutter.main.cookiecutter", fake_cookiecutter) + monkeypatch.setattr( + "veadk.utils.misc.load_module_from_file", + lambda **_kwargs: SimpleNamespace(main=fake_main), + ) + + result = CliRunner().invoke( + deploy, + [ + "--vefaas-app-name", + "studio-app", + "--path", + str(project), + ], + ) + + assert result.exit_code == 0, result.output + assert captured["extra_context"]["provider"] == "byteplus" + assert captured["extra_context"]["region"] == "ap-southeast-1" + assert captured["main_env"] == { + "CLOUD_PROVIDER": "byteplus", + "AGENTKIT_CLOUD_PROVIDER": "byteplus", + "BYTEPLUS_REGION": "ap-southeast-1", + "REGION": "ap-southeast-1", + "VOLCENGINE_ACCESS_KEY": "byteplus-ak", + "VOLCENGINE_SECRET_KEY": "byteplus-sk", + "VOLCENGINE_SESSION_TOKEN": "byteplus-token", + } diff --git a/tests/cli/test_frontend_sandbox_options.py b/tests/cli/test_frontend_sandbox_options.py index 2e358f465..f405f2b3a 100644 --- a/tests/cli/test_frontend_sandbox_options.py +++ b/tests/cli/test_frontend_sandbox_options.py @@ -34,14 +34,14 @@ def test_serve_provider_is_bootstrapped_before_command_modules_load( _bootstrap_serve_provider(["frontend"]) - assert os.environ["AGENTKIT_CLOUD_PROVIDER"] == "volcengine" - assert os.environ["CLOUD_PROVIDER"] == "volcengine" - - _bootstrap_serve_provider(["studio", "--provider", "byteplus"]) - assert os.environ["AGENTKIT_CLOUD_PROVIDER"] == "byteplus" assert os.environ["CLOUD_PROVIDER"] == "byteplus" + _bootstrap_serve_provider(["studio", "--provider", "volcengine"]) + + assert os.environ["AGENTKIT_CLOUD_PROVIDER"] == "volcengine" + assert os.environ["CLOUD_PROVIDER"] == "volcengine" + @pytest.mark.parametrize("command", [frontend, studio]) def test_sandbox_tool_options_are_shared_by_local_serve_commands( @@ -162,7 +162,7 @@ def test_local_studio_mounts_snapshot_tools_into_sandbox_services() -> None: @pytest.mark.parametrize("command", [frontend, studio]) -def test_local_serve_commands_default_to_volcengine( +def test_local_serve_commands_defer_provider_resolution_when_not_explicit( monkeypatch: pytest.MonkeyPatch, command: Command, ) -> None: @@ -176,7 +176,7 @@ def test_local_serve_commands_default_to_volcengine( result = CliRunner().invoke(command) assert result.exit_code == 0, result.output - assert captured["provider"] == "volcengine" + assert captured["provider"] is None @pytest.mark.parametrize("command", [frontend, studio]) diff --git a/tests/cli/test_generated_agent_backend_codegen_extended.py b/tests/cli/test_generated_agent_backend_codegen_extended.py index 93292a6dd..a336f22b1 100644 --- a/tests/cli/test_generated_agent_backend_codegen_extended.py +++ b/tests/cli/test_generated_agent_backend_codegen_extended.py @@ -88,6 +88,12 @@ } +@pytest.fixture(autouse=True) +def _default_to_volcengine_provider(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("CLOUD_PROVIDER", raising=False) + monkeypatch.delenv("AGENTKIT_CLOUD_PROVIDER", raising=False) + + def _file_map(project: GeneratedProject) -> dict[str, str]: return {file.path: file.content for file in project.files} diff --git a/tests/cli/test_studio_deploy_permissions.py b/tests/cli/test_studio_deploy_permissions.py index 4d8e52f01..9f9570634 100644 --- a/tests/cli/test_studio_deploy_permissions.py +++ b/tests/cli/test_studio_deploy_permissions.py @@ -14,6 +14,8 @@ from __future__ import annotations +import os + import pytest from click.testing import CliRunner @@ -357,6 +359,55 @@ def _precheck(*, specs, **_kwargs): assert "Pre-check only: no cloud resources were created." in result.output +def test_cli_restores_provider_process_env_after_byteplus_deploy( + monkeypatch, +) -> None: + for key in ( + "CLOUD_PROVIDER", + "AGENTKIT_CLOUD_PROVIDER", + "BYTEPLUS_REGION", + "BYTEPLUS_ACCESS_KEY", + "BYTEPLUS_SECRET_KEY", + "BYTEPLUS_SESSION_TOKEN", + "IAM_ROLE", + ): + monkeypatch.delenv(key, raising=False) + + def _precheck(*, specs, **_kwargs): + return [ + permissions.PermissionResult(spec=spec, satisfied=True) for spec in specs + ] + + monkeypatch.setattr( + permissions, + "run_studio_deploy_permission_precheck", + _precheck, + ) + + result = CliRunner().invoke( + studio, + [ + "deploy", + "--vefaas-app-name", + "studio-test", + "--provider", + "byteplus", + "--byteplus-access-key", + "ak", + "--byteplus-secret-key", + "sk", + "--precheck-only", + ], + ) + + assert result.exit_code == 0, result.output + assert os.environ.get("CLOUD_PROVIDER") is None + assert os.environ.get("AGENTKIT_CLOUD_PROVIDER") is None + assert os.environ.get("BYTEPLUS_REGION") is None + assert os.environ.get("BYTEPLUS_ACCESS_KEY") is None + assert os.environ.get("BYTEPLUS_SECRET_KEY") is None + + def test_cli_precheck_only_rejects_overlong_site_title_before_iam(monkeypatch) -> None: precheck_called = False diff --git a/tests/cli/test_studio_deploy_target.py b/tests/cli/test_studio_deploy_target.py index 56f55ea42..13797f0cc 100644 --- a/tests/cli/test_studio_deploy_target.py +++ b/tests/cli/test_studio_deploy_target.py @@ -47,6 +47,13 @@ @pytest.fixture(autouse=True) def _skip_serverless_role_setup(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("AGENTKIT_CLOUD_PROVIDER", raising=False) + monkeypatch.delenv("CLOUD_PROVIDER", raising=False) + monkeypatch.delenv("REGION", raising=False) + monkeypatch.delenv("BYTEPLUS_REGION", raising=False) + monkeypatch.delenv("BYTEPLUS_ACCESS_KEY", raising=False) + monkeypatch.delenv("BYTEPLUS_SECRET_KEY", raising=False) + monkeypatch.delenv("BYTEPLUS_SESSION_TOKEN", raising=False) monkeypatch.delenv("VOLCENGINE_SESSION_TOKEN", raising=False) monkeypatch.delenv("VOLC_SESSIONTOKEN", raising=False) monkeypatch.delenv("SANDBOX_CHAT_CODEX", raising=False) @@ -683,6 +690,8 @@ def _fail(**_: object) -> str: studio, [ "deploy", + "--provider", + "volcengine", "--user-pool-id", "pool-id", "--allowed-client-id", @@ -820,6 +829,8 @@ def _fail_after_tool_creation(**_: object) -> str: studio, [ "deploy", + "--provider", + "volcengine", "--user-pool-id", "pool-id", "--allowed-client-id", @@ -971,6 +982,8 @@ def configure_user_pool_for_idp_only(self, user_pool_uid: str) -> None: studio, [ "deploy", + "--provider", + "volcengine", "--user-pool-id", "pool-id", "--allowed-client-id", @@ -1111,6 +1124,8 @@ def deploy(self, **_: object) -> SimpleNamespace: studio, [ "deploy", + "--provider", + "volcengine", "--user-pool-id", "pool-id", "--allowed-client-id", @@ -1158,7 +1173,7 @@ def test_studio_deploy_byteplus_wires_provider_to_cloud_engine_and_package( ) -> None: captured: dict[str, object] = {} credential_tool_ids: list[str] = [] - monkeypatch.setenv("BYTEPLUS_REGION", "cn-beijing") + monkeypatch.setenv("BYTEPLUS_REGION", "ap-southeast-1") monkeypatch.setenv("BYTEPLUS_WEB_SEARCH_API_KEY", "bp-search-key") class _FakeCloudAgentEngine: diff --git a/tests/cli/test_studio_update.py b/tests/cli/test_studio_update.py index 88f28868c..6e0c88d0e 100644 --- a/tests/cli/test_studio_update.py +++ b/tests/cli/test_studio_update.py @@ -38,6 +38,19 @@ _PNG = b"\x89PNG\r\n\x1a\n" + b"0" * 32 +@pytest.fixture(autouse=True) +def _clear_provider_environment(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("AGENTKIT_CLOUD_PROVIDER", raising=False) + monkeypatch.delenv("CLOUD_PROVIDER", raising=False) + monkeypatch.delenv("REGION", raising=False) + monkeypatch.delenv("BYTEPLUS_REGION", raising=False) + monkeypatch.delenv("BYTEPLUS_ACCESS_KEY", raising=False) + monkeypatch.delenv("BYTEPLUS_SECRET_KEY", raising=False) + monkeypatch.delenv("BYTEPLUS_SESSION_TOKEN", raising=False) + monkeypatch.delenv("VOLCENGINE_SESSION_TOKEN", raising=False) + monkeypatch.delenv("VOLC_SESSIONTOKEN", raising=False) + + @pytest.fixture def scheduler_deploy( monkeypatch: pytest.MonkeyPatch, diff --git a/veadk/cli/cli.py b/veadk/cli/cli.py index 8709bf22f..32b26f7c5 100644 --- a/veadk/cli/cli.py +++ b/veadk/cli/cli.py @@ -27,7 +27,11 @@ def _bootstrap_serve_provider(argv: list[str] | None = None) -> None: if not args or args[0] not in {"frontend", "studio"}: return - provider = "volcengine" + provider = ( + os.environ.get("AGENTKIT_CLOUD_PROVIDER") + or os.environ.get("CLOUD_PROVIDER") + or "volcengine" + ) for index, argument in enumerate(args[1:]): if argument.startswith("--provider="): provider = argument.partition("=")[2] @@ -35,6 +39,9 @@ def _bootstrap_serve_provider(argv: list[str] | None = None) -> None: if argument == "--provider" and index + 2 < len(args): provider = args[index + 2] break + provider = provider.strip().lower() + if provider == "volces": + provider = "volcengine" if provider not in {"volcengine", "byteplus"}: return os.environ["AGENTKIT_CLOUD_PROVIDER"] = provider diff --git a/veadk/cli/cli_clean.py b/veadk/cli/cli_clean.py index a8e41057f..8544cd6b5 100644 --- a/veadk/cli/cli_clean.py +++ b/veadk/cli/cli_clean.py @@ -18,6 +18,63 @@ logger = get_logger(__name__) +def _resolve_clean_target( + provider: str | None, + region: str | None, +) -> tuple[str, str]: + from veadk.utils.cloud_provider import ( + CloudProvider, + cloud_provider_from_env, + default_region, + normalize_cloud_provider, + ) + + provider_id: CloudProvider = ( + normalize_cloud_provider(provider) if provider else cloud_provider_from_env() + ) + return provider_id, region or default_region(provider_id) + + +def _resolve_clean_credentials( + *, + provider: str, + volcengine_access_key: str | None, + volcengine_secret_key: str | None, + volcengine_session_token: str | None, + byteplus_access_key: str | None, + byteplus_secret_key: str | None, + byteplus_session_token: str | None, +) -> tuple[str, str, str]: + from veadk.config import getenv + + if provider == "byteplus": + access_key = byteplus_access_key or getenv( + "BYTEPLUS_ACCESS_KEY", "", allow_false_values=True + ) + secret_key = byteplus_secret_key or getenv( + "BYTEPLUS_SECRET_KEY", "", allow_false_values=True + ) + session_token = byteplus_session_token or getenv( + "BYTEPLUS_SESSION_TOKEN", "", allow_false_values=True + ) + if not access_key or not secret_key: + raise click.ClickException( + "BytePlus credentials required: pass --byteplus-access-key/" + "--byteplus-secret-key, or set BYTEPLUS_ACCESS_KEY/" + "BYTEPLUS_SECRET_KEY." + ) + return access_key, secret_key, session_token + + access_key = volcengine_access_key or getenv("VOLCENGINE_ACCESS_KEY") + secret_key = volcengine_secret_key or getenv("VOLCENGINE_SECRET_KEY") + session_token = ( + volcengine_session_token + or getenv("VOLCENGINE_SESSION_TOKEN", "", allow_false_values=True) + or getenv("VOLC_SESSIONTOKEN", "", allow_false_values=True) + ) + return access_key, secret_key, session_token + + @click.command() @click.option( "--vefaas-app-name", @@ -27,49 +84,84 @@ @click.option( "--volcengine-access-key", default=None, - help="Volcengine access key, if not set, will use the value of environment variable VOLCENGINE_ACCESS_KEY", + help=( + "Volcengine access key, if not set, will use the value of environment " + "variable VOLCENGINE_ACCESS_KEY" + ), ) @click.option( "--volcengine-secret-key", default=None, - help="Volcengine secret key, if not set, will use the value of environment variable VOLCENGINE_SECRET_KEY", + help=( + "Volcengine secret key, if not set, will use the value of environment " + "variable VOLCENGINE_SECRET_KEY" + ), +) +@click.option( + "--volcengine-session-token", + default=None, + help="Volcengine session token, if not set, will use VOLCENGINE_SESSION_TOKEN", +) +@click.option("--byteplus-access-key", default=None, envvar="BYTEPLUS_ACCESS_KEY") +@click.option("--byteplus-secret-key", default=None, envvar="BYTEPLUS_SECRET_KEY") +@click.option("--byteplus-session-token", default=None, envvar="BYTEPLUS_SESSION_TOKEN") +@click.option( + "--provider", + type=click.Choice(["volcengine", "byteplus"]), + default=None, + help=( + "Cloud provider to clean. Defaults to " + "AGENTKIT_CLOUD_PROVIDER/CLOUD_PROVIDER, then volcengine." + ), +) +@click.option( + "--region", + default=None, + help=( + "Cloud region to clean. Defaults to BYTEPLUS_REGION for BytePlus, or " + "REGION/cn-beijing for Volcengine." + ), ) def clean( - vefaas_app_name: str, volcengine_access_key: str, volcengine_secret_key: str + vefaas_app_name: str, + volcengine_access_key: str | None, + volcengine_secret_key: str | None, + volcengine_session_token: str | None, + byteplus_access_key: str | None, + byteplus_secret_key: str | None, + byteplus_session_token: str | None, + provider: str | None, + region: str | None, ) -> None: - """ - Clean and delete a VeFaaS application from the cloud. - - This command deletes a specified VeFaaS application after user confirmation. - It will prompt the user for confirmation before proceeding with the deletion - and monitor the deletion process until completion. - - Args: - vefaas_app_name (str): The name of the VeFaaS application to delete - volcengine_access_key (str): Volcengine access key for authentication. - If None, will use VOLCENGINE_ACCESS_KEY environment variable - volcengine_secret_key (str): Volcengine secret key for authentication. - If None, will use VOLCENGINE_SECRET_KEY environment variable - - Returns: - None - """ + """Clean and delete a VeFaaS application from the cloud.""" import time - from veadk.config import getenv from veadk.integrations.ve_faas.ve_faas import VeFaaS - if not volcengine_access_key: - volcengine_access_key = getenv("VOLCENGINE_ACCESS_KEY") - if not volcengine_secret_key: - volcengine_secret_key = getenv("VOLCENGINE_SECRET_KEY") + provider_id, resolved_region = _resolve_clean_target(provider, region) + access_key, secret_key, session_token = _resolve_clean_credentials( + provider=provider_id, + volcengine_access_key=volcengine_access_key, + volcengine_secret_key=volcengine_secret_key, + volcengine_session_token=volcengine_session_token, + byteplus_access_key=byteplus_access_key, + byteplus_secret_key=byteplus_secret_key, + byteplus_session_token=byteplus_session_token, + ) - confirm = input(f"Confirm delete cloud app {vefaas_app_name}? (y/N): ") + confirm = input( + f"Confirm delete cloud app {vefaas_app_name} " + f"from {provider_id}/{resolved_region}? (y/N): " + ) if confirm.lower() != "y": click.echo("Delete cancelled.") return else: vefaas_client = VeFaaS( - access_key=volcengine_access_key, secret_key=volcengine_secret_key + access_key=access_key, + secret_key=secret_key, + session_token=session_token, + region=resolved_region, + provider=provider_id, ) vefaas_application_id = vefaas_client.find_app_id_by_name(vefaas_app_name) vefaas_client.delete(vefaas_application_id) diff --git a/veadk/cli/cli_deploy.py b/veadk/cli/cli_deploy.py index 4f8149c59..d4626ca01 100644 --- a/veadk/cli/cli_deploy.py +++ b/veadk/cli/cli_deploy.py @@ -12,12 +12,43 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os import click from veadk.version import VERSION TEMP_PATH = "/tmp" +_DEPLOY_PROCESS_ENV_KEYS = ( + "CLOUD_PROVIDER", + "AGENTKIT_CLOUD_PROVIDER", + "BYTEPLUS_REGION", + "BYTEPLUS_ACCESS_KEY", + "BYTEPLUS_SECRET_KEY", + "BYTEPLUS_SESSION_TOKEN", + "VOLCENGINE_ACCESS_KEY", + "VOLCENGINE_SECRET_KEY", + "VOLCENGINE_SESSION_TOKEN", + "VOLC_SESSIONTOKEN", + "REGION", + "IAM_ROLE", +) + + +def _restore_process_env_on_click_close(keys: tuple[str, ...]) -> None: + ctx = click.get_current_context(silent=True) + if ctx is None: + return + original = {key: os.environ.get(key) for key in keys} + + def _restore() -> None: + for key, value in original.items(): + if value is None: + os.environ.pop(key, None) + else: + os.environ[key] = value + + ctx.call_on_close(_restore) @click.command() @@ -31,6 +62,28 @@ default=None, help="Volcengine secret key", ) +@click.option( + "--volcengine-session-token", + default=None, + help="Volcengine session token", +) +@click.option("--byteplus-access-key", default=None, envvar="BYTEPLUS_ACCESS_KEY") +@click.option("--byteplus-secret-key", default=None, envvar="BYTEPLUS_SECRET_KEY") +@click.option("--byteplus-session-token", default=None, envvar="BYTEPLUS_SESSION_TOKEN") +@click.option( + "--provider", + type=click.Choice(["volcengine", "byteplus"]), + default=None, + help="Cloud provider. Defaults to AGENTKIT_CLOUD_PROVIDER/CLOUD_PROVIDER, then volcengine.", +) +@click.option( + "--region", + default=None, + help=( + "Cloud region. Defaults to BYTEPLUS_REGION for BytePlus, or " + "REGION/cn-beijing for Volcengine." + ), +) @click.option( "--vefaas-app-name", required=True, help="Expected Volcengine FaaS application name" ) @@ -71,6 +124,12 @@ def deploy( volcengine_access_key: str, volcengine_secret_key: str, + volcengine_session_token: str, + byteplus_access_key: str, + byteplus_secret_key: str, + byteplus_session_token: str, + provider: str | None, + region: str | None, vefaas_app_name: str, veapig_instance_name: str, veapig_service_name: str, @@ -102,6 +161,12 @@ def deploy( will use VOLCENGINE_ACCESS_KEY environment variable volcengine_secret_key: Volcengine secret key for API authentication. If not provided, will use VOLCENGINE_SECRET_KEY environment variable + volcengine_session_token: Volcengine session token for API authentication. + byteplus_access_key: BytePlus access key for API authentication. + byteplus_secret_key: BytePlus secret key for API authentication. + byteplus_session_token: BytePlus session token for API authentication. + provider: Cloud provider for the deployment. + region: Cloud region for the deployment. vefaas_app_name: Name of the target Volcengine FaaS application where the project will be deployed veapig_instance_name: Optional Volcengine API Gateway instance name for @@ -130,18 +195,73 @@ def deploy( from cookiecutter.main import cookiecutter import veadk.integrations.ve_faas as vefaas - from veadk.config import getenv + from veadk.config import getenv, veadk_environments from veadk.utils.logger import get_logger from veadk.utils.misc import formatted_timestamp, load_module_from_file - import os - from veadk.config import veadk_environments + from veadk.utils.cloud_provider import ( + cloud_provider_from_env, + default_region, + normalize_cloud_provider, + ) logger = get_logger(__name__) - if not volcengine_access_key: - volcengine_access_key = getenv("VOLCENGINE_ACCESS_KEY") - if not volcengine_secret_key: - volcengine_secret_key = getenv("VOLCENGINE_SECRET_KEY") + _restore_process_env_on_click_close(_DEPLOY_PROCESS_ENV_KEYS) + provider_id = ( + normalize_cloud_provider(provider) if provider else cloud_provider_from_env() + ) + resolved_region = region or default_region(provider_id) + os.environ["CLOUD_PROVIDER"] = provider_id + os.environ["AGENTKIT_CLOUD_PROVIDER"] = provider_id + veadk_environments["CLOUD_PROVIDER"] = provider_id + veadk_environments["AGENTKIT_CLOUD_PROVIDER"] = provider_id + + if provider_id == "byteplus": + if not byteplus_access_key: + byteplus_access_key = getenv( + "BYTEPLUS_ACCESS_KEY", "", allow_false_values=True + ) + if not byteplus_secret_key: + byteplus_secret_key = getenv( + "BYTEPLUS_SECRET_KEY", "", allow_false_values=True + ) + if not byteplus_access_key or not byteplus_secret_key: + raise click.ClickException( + "BytePlus credentials required: pass --byteplus-access-key/" + "--byteplus-secret-key, or set BYTEPLUS_ACCESS_KEY/" + "BYTEPLUS_SECRET_KEY." + ) + byteplus_session_token = byteplus_session_token or getenv( + "BYTEPLUS_SESSION_TOKEN", "", allow_false_values=True + ) + volcengine_access_key = byteplus_access_key + volcengine_secret_key = byteplus_secret_key + volcengine_session_token = byteplus_session_token + os.environ["BYTEPLUS_ACCESS_KEY"] = byteplus_access_key + os.environ["BYTEPLUS_SECRET_KEY"] = byteplus_secret_key + os.environ["BYTEPLUS_REGION"] = resolved_region + veadk_environments["BYTEPLUS_REGION"] = resolved_region + if byteplus_session_token: + os.environ["BYTEPLUS_SESSION_TOKEN"] = byteplus_session_token + else: + if not volcengine_access_key: + volcengine_access_key = getenv("VOLCENGINE_ACCESS_KEY") + if not volcengine_secret_key: + volcengine_secret_key = getenv("VOLCENGINE_SECRET_KEY") + volcengine_session_token = ( + volcengine_session_token + or getenv("VOLCENGINE_SESSION_TOKEN", "", allow_false_values=True) + or getenv("VOLC_SESSIONTOKEN", "", allow_false_values=True) + ) + + os.environ["VOLCENGINE_ACCESS_KEY"] = volcengine_access_key + os.environ["VOLCENGINE_SECRET_KEY"] = volcengine_secret_key + if volcengine_session_token: + os.environ["VOLCENGINE_SESSION_TOKEN"] = volcengine_session_token + else: + os.environ.pop("VOLCENGINE_SESSION_TOKEN", None) + os.environ["REGION"] = resolved_region + if not iam_role: iam_role = getenv("IAM_ROLE", None, allow_false_values=True) else: @@ -152,9 +272,11 @@ def deploy( template_dir_path = Path(vefaas.__file__).parent / "template" tmp_dir_name = f"{user_proj_abs_path.name}_{formatted_timestamp()}" + rendered_tmp_dir_name = tmp_dir_name.replace("-", "_") + tmp_dir_path = Path(TEMP_PATH) / rendered_tmp_dir_name settings = { - "local_dir_name": tmp_dir_name.replace("-", "_"), + "local_dir_name": rendered_tmp_dir_name, "app_name": user_proj_abs_path.name.replace("-", "_"), "agent_module_name": user_proj_abs_path.name, "short_term_memory_backend": short_term_memory_backend, @@ -166,6 +288,8 @@ def deploy( "auth_method": auth_method, "veidentity_user_pool_name": user_pool_name, "veidentity_client_name": client_name, + "provider": provider_id, + "region": resolved_region, "veadk_version": VERSION, } @@ -175,14 +299,9 @@ def deploy( no_input=True, extra_context=settings, ) - logger.debug(f"Create a template project at {TEMP_PATH}/{tmp_dir_name}") + logger.debug(f"Create a template project at {tmp_dir_path}") - agent_dir = ( - Path(TEMP_PATH) - / tmp_dir_name - / "src" - / user_proj_abs_path.name.replace("-", "_") - ) + agent_dir = tmp_dir_path / "src" / user_proj_abs_path.name.replace("-", "_") # remove /tmp/tmp_dir_name/src/user_proj_abs_path.name shutil.rmtree(agent_dir) @@ -199,7 +318,7 @@ def deploy( ) shutil.copy( user_proj_abs_path / "requirements.txt", - Path(TEMP_PATH) / tmp_dir_name / "src" / "requirements.txt", + tmp_dir_path / "src" / "requirements.txt", ) else: logger.warning( @@ -211,23 +330,21 @@ def deploy( logger.warning( f"Find a config.yaml in {user_proj_abs_path}/config.yaml, we will not upload it by default." ) - shutil.move(agent_dir / "config.yaml", Path(TEMP_PATH) / tmp_dir_name) + shutil.move(agent_dir / "config.yaml", tmp_dir_path) else: logger.info( "No config.yaml found in the user project. Some environment variables may not be set." ) # load - logger.debug( - f"Load deploy module from {Path(TEMP_PATH) / tmp_dir_name / 'deploy.py'}" - ) + logger.debug(f"Load deploy module from {tmp_dir_path / 'deploy.py'}") deploy_module = load_module_from_file( module_name="deploy_module", - file_path=str(Path(TEMP_PATH) / tmp_dir_name / "deploy.py"), + file_path=str(tmp_dir_path / "deploy.py"), ) - logger.info(f"Begin deploy from {Path(TEMP_PATH) / tmp_dir_name / 'src'}") + logger.info(f"Begin deploy from {tmp_dir_path / 'src'}") asyncio.run(deploy_module.main()) # remove tmp file logger.info("Deploy done. Delete temp dir.") - shutil.rmtree(Path(TEMP_PATH) / tmp_dir_name) + shutil.rmtree(tmp_dir_path) diff --git a/veadk/cli/cli_frontend.py b/veadk/cli/cli_frontend.py index 69aa9f64c..137ea75f3 100644 --- a/veadk/cli/cli_frontend.py +++ b/veadk/cli/cli_frontend.py @@ -68,6 +68,7 @@ DEFAULT_CLOUD_PROVIDER, CloudProvider, agentkit_openapi_base, + cloud_provider_from_env, default_region, default_vefaas_application_template_id, normalize_cloud_provider, @@ -80,6 +81,15 @@ _BYTEPLUS_VEFAAS_APPLICATION_NAME_RE = re.compile( r"^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$" ) +_STUDIO_DEPLOY_PROCESS_ENV_KEYS = ( + "CLOUD_PROVIDER", + "AGENTKIT_CLOUD_PROVIDER", + "BYTEPLUS_REGION", + "BYTEPLUS_ACCESS_KEY", + "BYTEPLUS_SECRET_KEY", + "BYTEPLUS_SESSION_TOKEN", + "IAM_ROLE", +) _BUILD_ERROR_MARKERS = ( "no solution found", "unsatisfiable", @@ -93,6 +103,36 @@ "permission denied", "traceback (most recent call last)", ) + + +def _capture_process_env(keys: Iterable[str]) -> Callable[[], None]: + original = {key: os.environ.get(key) for key in keys} + + def _restore() -> None: + for key, value in original.items(): + if value is None: + os.environ.pop(key, None) + else: + os.environ[key] = value + + return _restore + + +def _restore_process_env_on_click_close(keys: Iterable[str]) -> None: + ctx = click.get_current_context(silent=True) + if ctx is None: + return + _restore = _capture_process_env(keys) + ctx.call_on_close(_restore) + + +def _click_param_from_commandline(name: str) -> bool: + ctx = click.get_current_context(silent=True) + if ctx is None: + return False + return ctx.get_parameter_source(name) == click.core.ParameterSource.COMMANDLINE + + _SENSITIVE_LOG_PATTERNS = ( re.compile(r"authorization\s*[:=]", re.IGNORECASE), re.compile(r"\bbearer\s+\S+", re.IGNORECASE), @@ -1047,11 +1087,10 @@ def _serve_options(f): click.option( "--provider", type=click.Choice(["volcengine", "byteplus"]), - default="volcengine", - show_default=True, + default=None, help=( - "Cloud provider for AgentKit services. BytePlus is used only " - "when explicitly selected." + "Cloud provider for AgentKit services. Defaults to " + "AGENTKIT_CLOUD_PROVIDER/CLOUD_PROVIDER, then volcengine." ), ), click.option( @@ -1385,7 +1424,7 @@ def _run_frontend_server( studio_admins: str | None = None, studio_developers: str | None = None, open_browser: bool, - provider: Literal["volcengine", "byteplus"] = "volcengine", + provider: Literal["volcengine", "byteplus"] | None = None, studio: bool = False, ) -> None: """Launch the A2UI web UI backed by the ADK agent API server.""" @@ -1407,14 +1446,15 @@ def _run_frontend_server( else: logger.warning("No .env file found in current directory or parent directories") - # The local CLI is Volcengine-first even when a shell or global AgentKit - # config previously selected BytePlus. BytePlus requires the explicit - # ``--provider byteplus`` opt-in. - os.environ["AGENTKIT_CLOUD_PROVIDER"] = provider - os.environ["CLOUD_PROVIDER"] = provider + provider_id = ( + normalize_cloud_provider(provider) if provider else cloud_provider_from_env() + ) + provider = provider_id + os.environ["AGENTKIT_CLOUD_PROVIDER"] = provider_id + os.environ["CLOUD_PROVIDER"] = provider_id from agentkit.platform.context import set_default_cloud_provider - set_default_cloud_provider(provider) + set_default_cloud_provider(provider_id) if sandbox_chat_codex_tool_id: os.environ["SANDBOX_CHAT_CODEX"] = sandbox_chat_codex_tool_id @@ -1468,7 +1508,7 @@ def _run_frontend_server( studio_tool_registry.revision, ) - studio_route_registry = build_studio_route_registry(provider=provider) + studio_route_registry = build_studio_route_registry(provider=provider_id) studio_route_channels = StudioRouteChannelManager(studio_route_registry) app.state.studio_route_registry = studio_route_registry app.state.studio_route_channels = studio_route_channels @@ -10670,9 +10710,11 @@ def _resolve_studio_cloud_credentials( @click.option( "--provider", type=click.Choice(["volcengine", "byteplus"]), - default="volcengine", - show_default=True, - help="Cloud provider for Studio deployment.", + default=None, + help=( + "Cloud provider for Studio deployment. Defaults to " + "AGENTKIT_CLOUD_PROVIDER/CLOUD_PROVIDER, then volcengine." + ), ) @click.option( "--region", @@ -10843,7 +10885,7 @@ def frontend_deploy( client_secret: str, allow_dangerous_login: bool, vefaas_app_name: str, - provider: str, + provider: str | None, region: str | None, project: str, iam_role: str | None, @@ -10891,11 +10933,32 @@ def frontend_deploy( ) from veadk.config import veadk_environments - provider_id = normalize_cloud_provider(provider) - if provider_id == "byteplus": - region = region or DEFAULT_BYTEPLUS_REGION + _restore_process_env_on_click_close(_STUDIO_DEPLOY_PROCESS_ENV_KEYS) + explicit_volcengine_credentials = any( + _click_param_from_commandline(name) + for name in ( + "volcengine_access_key", + "volcengine_secret_key", + "volcengine_session_token", + ) + ) + explicit_byteplus_credentials = any( + _click_param_from_commandline(name) + for name in ( + "byteplus_access_key", + "byteplus_secret_key", + "byteplus_session_token", + ) + ) + if provider: + provider_id = normalize_cloud_provider(provider) + elif explicit_volcengine_credentials and not explicit_byteplus_credentials: + provider_id = "volcengine" + elif explicit_byteplus_credentials and not explicit_volcengine_credentials: + provider_id = "byteplus" else: - region = region or default_region(provider_id) + provider_id = cloud_provider_from_env() + region = region or default_region(provider_id) os.environ["CLOUD_PROVIDER"] = provider_id os.environ["AGENTKIT_CLOUD_PROVIDER"] = provider_id if provider_id == "byteplus": @@ -11668,10 +11731,12 @@ def frontend_deploy( @studio.command("update") @click.option( "--provider", - default=DEFAULT_CLOUD_PROVIDER, + default=None, type=click.Choice(["volcengine", "byteplus"]), - show_default=True, - help="Cloud provider for the existing Studio deployment.", + help=( + "Cloud provider for the existing Studio deployment. Defaults to " + "AGENTKIT_CLOUD_PROVIDER/CLOUD_PROVIDER, then volcengine." + ), ) @click.option( "--vefaas-app-name", @@ -11755,7 +11820,7 @@ def frontend_deploy( @click.option("--byteplus-secret-key", default=None, envvar="BYTEPLUS_SECRET_KEY") @click.option("--byteplus-session-token", default=None, envvar="BYTEPLUS_SESSION_TOKEN") def frontend_update( - provider: str, + provider: str | None, vefaas_app_name: str, region: str | None, project: str | None, @@ -11797,14 +11862,16 @@ def frontend_update( ) from veadk.integrations.ve_faas.ve_faas import VeFaaS - provider_id = normalize_cloud_provider(provider) + provider_id = ( + normalize_cloud_provider(provider) if provider else cloud_provider_from_env() + ) if provider_id == "byteplus": - if region is not None and region != DEFAULT_BYTEPLUS_REGION: + region = region or default_region(provider_id) + if region != DEFAULT_BYTEPLUS_REGION: raise click.ClickException( "BytePlus Studio update currently supports only " f"{DEFAULT_BYTEPLUS_REGION}; got {region}." ) - region = region or DEFAULT_BYTEPLUS_REGION elif region == DEFAULT_BYTEPLUS_REGION: raise click.ClickException( f"{DEFAULT_BYTEPLUS_REGION} is a BytePlus region. Use " diff --git a/veadk/integrations/ve_faas/template/cookiecutter.json b/veadk/integrations/ve_faas/template/cookiecutter.json index 0191c1e2b..a43b299db 100644 --- a/veadk/integrations/ve_faas/template/cookiecutter.json +++ b/veadk/integrations/ve_faas/template/cookiecutter.json @@ -11,5 +11,7 @@ "auth_method": "none", "veidentity_user_pool_name": "", "veidentity_client_name": "", + "provider": "volcengine", + "region": "cn-beijing", "veadk_version": "" -} \ No newline at end of file +} diff --git a/veadk/integrations/ve_faas/template/{{cookiecutter.local_dir_name}}/deploy.py b/veadk/integrations/ve_faas/template/{{cookiecutter.local_dir_name}}/deploy.py index c82927623..51abebe15 100644 --- a/veadk/integrations/ve_faas/template/{{cookiecutter.local_dir_name}}/deploy.py +++ b/veadk/integrations/ve_faas/template/{{cookiecutter.local_dir_name}}/deploy.py @@ -75,7 +75,10 @@ async def _send_msg_with_mcp(cloud_app: CloudApp, message: str) -> None: async def main(): - engine = CloudAgentEngine() + engine = CloudAgentEngine( + provider="{{cookiecutter.provider}}", + region="{{cookiecutter.region}}", + ) cloud_app = engine.deploy( path=str(Path(__file__).parent / "src"), diff --git a/veadk/integrations/ve_faas/web_template/cookiecutter.json b/veadk/integrations/ve_faas/web_template/cookiecutter.json index 65ee0b223..634e1d977 100644 --- a/veadk/integrations/ve_faas/web_template/cookiecutter.json +++ b/veadk/integrations/ve_faas/web_template/cookiecutter.json @@ -9,6 +9,8 @@ "auth_method": "none", "veidentity_user_pool_name": "", "veidentity_client_name": "", + "provider": "volcengine", + "region": "cn-beijing", "veadk_version": "", "_copy_without_render": [ "*.html", @@ -17,4 +19,4 @@ "static/**/*", "templates/**/*" ] -} \ No newline at end of file +} diff --git a/veadk/integrations/ve_faas/web_template/{{cookiecutter.local_dir_name}}/deploy.py b/veadk/integrations/ve_faas/web_template/{{cookiecutter.local_dir_name}}/deploy.py index eb9579196..c3c8bfa4a 100644 --- a/veadk/integrations/ve_faas/web_template/{{cookiecutter.local_dir_name}}/deploy.py +++ b/veadk/integrations/ve_faas/web_template/{{cookiecutter.local_dir_name}}/deploy.py @@ -18,7 +18,10 @@ from veadk.cloud.cloud_agent_engine import CloudAgentEngine async def main(): - engine = CloudAgentEngine() + engine = CloudAgentEngine( + provider="{{cookiecutter.provider}}", + region="{{cookiecutter.region}}", + ) cloud_app = engine.deploy( path=str(Path(__file__).parent / "src"),