Skip to content
Draft
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
1 change: 1 addition & 0 deletions tests/cli/test_frontend_deploy_iam.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ def test_frontend_policy_allows_release_download() -> None:
assert "iam:UpdatePolicy" not in actions
assert "vefaas:CodeUploadCallback" in actions
assert "vefaas:UpdateFunction" in actions
assert "vefaas:UpdateFunctionResource" in actions
assert "vefaas:ReleaseApplication" in actions


Expand Down
5 changes: 3 additions & 2 deletions tests/cli/test_studio_deploy_permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def test_default_studio_deploy_requires_all_reachable_actions() -> None:
specs = _default_specs()
actions = [spec.action for spec in specs]

assert len(actions) == 39
assert len(actions) == 40
assert len(actions) == len(set(actions))
assert "id:CreateUserPool" in actions
assert "iam:UpdatePolicy" in actions
Expand All @@ -45,6 +45,7 @@ def test_default_studio_deploy_requires_all_reachable_actions() -> None:
assert "apig:CreateGateway" in actions
assert "apig:UpdateRoute" in actions
assert "vefaas:CreateApplication" in actions
assert "vefaas:UpdateFunctionResource" in actions
assert "vefaas:DeleteApplication" in actions


Expand Down Expand Up @@ -274,7 +275,7 @@ def _precheck(*, specs, **_kwargs):
)

assert result.exit_code == 0, result.output
assert "All 39 required IAM Actions are satisfied." in result.output
assert "All 40 required IAM Actions are satisfied." in result.output
assert "Pre-check only: no cloud resources were created." in result.output


Expand Down
7 changes: 6 additions & 1 deletion tests/cli/test_studio_self_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,13 +231,14 @@ def __init__(self, **kwargs: str) -> None:
captured["credentials"] = kwargs
self.client = object()

def submit_application_code_bundle_update(self, **kwargs: Any) -> None:
def submit_application_code_bundle_update(self, **kwargs: Any) -> bool:
package = Path(str(kwargs["path"]))
assert "--provider byteplus" in (package / "run.sh").read_text(
encoding="utf-8"
)
assert (package / "requirements.txt").is_file()
captured["update"] = kwargs
return False

def _resources(**kwargs: Any) -> dict[str, str]:
captured["resource_request"] = kwargs
Expand Down Expand Up @@ -301,6 +302,10 @@ def _resources(**kwargs: Any) -> dict[str, str]:
assert status["progressMessage"] == "已提交,正在等待新 Revision 发布"
assert status["targetVersion"] == manifest.version
assert status["startedAt"] > 0
assert any(
"vefaas:UpdateFunctionResource" in line and "MinInstance 保持原值" in line
for line in status["updateLogs"]
)


def test_submit_latest_reports_missing_vefaas_permissions(
Expand Down
94 changes: 94 additions & 0 deletions tests/cli/test_studio_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -1142,6 +1142,7 @@ def test_update_application_code_bundle_merges_only_explicit_environment(
tmp_path: Path,
) -> None:
updated_requests: list[Any] = []
resource_requests: list[Any] = []
service = object.__new__(VeFaaS)
service.session_token = ""
cast(Any, service).client = SimpleNamespace(
Expand All @@ -1152,6 +1153,7 @@ def test_update_application_code_bundle_merges_only_explicit_environment(
]
),
update_function=updated_requests.append,
update_function_resource=resource_requests.append,
)
monkeypatch.setattr(service, "_upload_and_mount_code", lambda *_: None)
monkeypatch.setattr(service, "_release_application", lambda _: "https://same")
Expand All @@ -1170,6 +1172,96 @@ def test_update_application_code_bundle_merges_only_explicit_environment(
"EXISTING": "kept",
"VEADK_SITE_TITLE": "新标题",
}
resource_request = resource_requests[0]
assert resource_request.function_id == "function-id"
assert resource_request.min_instance == 1
assert resource_request.max_instance is None
assert resource_request.reserved_frozen_instance is None


def test_submit_application_code_bundle_sets_only_minimum_instance(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
updated_requests: list[Any] = []
resource_requests: list[Any] = []
releases: list[str] = []
service = object.__new__(VeFaaS)
cast(Any, service).client = SimpleNamespace(
update_function=updated_requests.append,
update_function_resource=resource_requests.append,
)
monkeypatch.setattr(service, "_upload_and_mount_code", lambda *_: None)
monkeypatch.setattr(service, "_start_application_release", releases.append)

minimum_instance_updated = service.submit_application_code_bundle_update(
application_id="app-id",
function_id="function-id",
path=str(tmp_path),
)

assert updated_requests[0].id == "function-id"
resource_request = resource_requests[0]
assert resource_request.function_id == "function-id"
assert resource_request.min_instance == 1
assert resource_request.max_instance is None
assert resource_request.reserved_frozen_instance is None
assert releases == ["app-id"]
assert minimum_instance_updated is True


def test_submit_application_code_bundle_continues_without_resource_permission(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
releases: list[str] = []
service = object.__new__(VeFaaS)

def _permission_denied(_: Any) -> None:
raise RuntimeError("AccessDenied: vefaas:UpdateFunctionResource")

cast(Any, service).client = SimpleNamespace(
update_function=lambda _: None,
update_function_resource=_permission_denied,
)
monkeypatch.setattr(service, "_upload_and_mount_code", lambda *_: None)
monkeypatch.setattr(service, "_start_application_release", releases.append)

minimum_instance_updated = service.submit_application_code_bundle_update(
application_id="app-id",
function_id="function-id",
path=str(tmp_path),
)

assert minimum_instance_updated is False
assert releases == ["app-id"]


def test_submit_application_code_bundle_stops_on_other_resource_errors(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
releases: list[str] = []
service = object.__new__(VeFaaS)

def _invalid_operation(_: Any) -> None:
raise RuntimeError("InvalidOperation")

cast(Any, service).client = SimpleNamespace(
update_function=lambda _: None,
update_function_resource=_invalid_operation,
)
monkeypatch.setattr(service, "_upload_and_mount_code", lambda *_: None)
monkeypatch.setattr(service, "_start_application_release", releases.append)

with pytest.raises(RuntimeError, match="InvalidOperation"):
service.submit_application_code_bundle_update(
application_id="app-id",
function_id="function-id",
path=str(tmp_path),
)

assert releases == []


def test_application_operations_use_deployment_region(
Expand Down Expand Up @@ -1452,6 +1544,7 @@ def test_update_application_code_bundle_preserves_unspecified_sandbox_tool(
]
),
update_function=updated_requests.append,
update_function_resource=lambda _: None,
)
monkeypatch.setattr(service, "_upload_and_mount_code", lambda *_: None)
monkeypatch.setattr(service, "_release_application", lambda _: "https://same")
Expand Down Expand Up @@ -1480,6 +1573,7 @@ def test_update_application_code_bundle_does_not_read_or_replace_environment(
cast(Any, service).client = SimpleNamespace(
get_function=lambda _: pytest.fail("environment should not be read"),
update_function=updated_requests.append,
update_function_resource=lambda _: None,
)
monkeypatch.setattr(service, "_upload_and_mount_code", lambda *_: None)
monkeypatch.setattr(service, "_release_application", lambda _: "https://same")
Expand Down
75 changes: 75 additions & 0 deletions tests/test_cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import os
import tempfile
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import AsyncMock, Mock, patch

Expand Down Expand Up @@ -47,9 +48,27 @@ def test_vefaas_create_function_uses_configured_project() -> None:
assert request.project_name == "studio-project"


@pytest.mark.parametrize("provider", ["volcengine", "byteplus"])
def test_vefaas_sets_only_minimum_instance(provider: str) -> None:
requests = []
service = object.__new__(VeFaaS)
service.provider = provider
service.client = SimpleNamespace(update_function_resource=requests.append)

service._set_function_min_instance("function-id")

assert len(requests) == 1
request = requests[0]
assert request.function_id == "function-id"
assert request.min_instance == 1
assert request.max_instance is None
assert request.reserved_frozen_instance is None


def test_vefaas_deploy_cleans_created_resources_on_release_failure() -> None:
service = object.__new__(VeFaaS)
service._create_function = Mock(return_value=("studio-app-fn", "function-id"))
service._set_function_min_instance = Mock()
service._create_application = Mock(return_value="application-id")
service._release_application = Mock(side_effect=RuntimeError("release failed"))
service.delete = Mock()
Expand All @@ -66,11 +85,13 @@ def test_vefaas_deploy_cleans_created_resources_on_release_failure() -> None:

service.delete.assert_called_once_with("application-id")
service.delete_function.assert_called_once_with("function-id")
service._set_function_min_instance.assert_not_called()


def test_vefaas_deploy_can_keep_failed_resources_for_inspection() -> None:
service = object.__new__(VeFaaS)
service._create_function = Mock(return_value=("studio-app-fn", "function-id"))
service._set_function_min_instance = Mock()
service._create_application = Mock(return_value="application-id")
service._release_application = Mock(side_effect=RuntimeError("release failed"))
service.delete = Mock()
Expand All @@ -88,6 +109,60 @@ def test_vefaas_deploy_can_keep_failed_resources_for_inspection() -> None:

service.delete.assert_not_called()
service.delete_function.assert_not_called()
service._set_function_min_instance.assert_not_called()


def test_vefaas_update_code_uses_resource_updating_bundle_path(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
project = tmp_path / "agent"
project.mkdir()
service = object.__new__(VeFaaS)
service.find_app_id_by_name = Mock(return_value="application-id")
service._get_application_status = Mock(
return_value=(
"deploy_success",
{
"Result": {
"CloudResource": (
'{"framework":{"function":{"Name":"studio-app-fn",'
'"Id":"function-id"}}}'
)
}
},
)
)
service._replace_application_code_bundle = Mock()
service._release_application = Mock(return_value="https://studio.example")
service._set_function_min_instance = Mock()
service.ensure_application_route_methods = Mock()

def _cookiecutter(*, output_dir: str, extra_context: dict, **_: object) -> None:
package = Path(output_dir) / str(extra_context["local_dir_name"])
(package / "src").mkdir(parents=True)

monkeypatch.setattr(
"veadk.integrations.ve_faas.ve_faas.formatted_timestamp", lambda: "stamp"
)
monkeypatch.setattr(
"veadk.integrations.ve_faas.ve_faas.tempfile.gettempdir",
lambda: str(tmp_path),
)
monkeypatch.setattr(
"veadk.integrations.ve_faas.ve_faas.cookiecutter", _cookiecutter
)

result = service._update_function_code("studio-app", str(project))

assert result == ("https://studio.example", "application-id", "function-id")
service._replace_application_code_bundle.assert_called_once_with(
function_id="function-id",
path=str(tmp_path / "agent_update_stamp" / "src"),
environment_overrides=None,
request_timeout=1800,
)
service._set_function_min_instance.assert_called_once_with("function-id")


def test_apig_uses_session_token() -> None:
Expand Down
1 change: 1 addition & 0 deletions veadk/cli/frontend_deploy_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@
"vefaas:ReleaseApplication",
"vefaas:SetSandboxTimeout",
"vefaas:UpdateFunction",
"vefaas:UpdateFunctionResource",
"vikingdb:GetKnowledgeBaseServiceInfo",
"vikingdb:GetMemorydbInstanceDetail",
"vikingdb:ListCollections",
Expand Down
5 changes: 5 additions & 0 deletions veadk/cli/studio_deploy_permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,11 @@ def _permission(action: str, purpose_zh: str, purpose_en: str) -> PermissionSpec
"更新 Studio 函数环境变量",
"Update Studio function environment variables",
),
_permission(
"vefaas:UpdateFunctionResource",
"设置 Studio 函数最小实例数",
"Set the Studio function minimum instance count",
),
_permission(
"vefaas:Release", "重新发布 Studio 函数", "Release the updated Studio function"
),
Expand Down
19 changes: 14 additions & 5 deletions veadk/cli/studio_self_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -410,12 +410,21 @@ def submit_version(self, version: str | None) -> StudioReleaseManifest:
}
)
self._set_progress("submitting", "正在提交 VeFaaS Function 更新")
service.submit_application_code_bundle_update(
application_id=self._settings.application_id,
function_id=self._settings.function_id,
path=str(package_dir),
environment_overrides=environment_overrides,
minimum_instance_updated = (
service.submit_application_code_bundle_update(
application_id=self._settings.application_id,
function_id=self._settings.function_id,
path=str(package_dir),
environment_overrides=environment_overrides,
)
)
if minimum_instance_updated is False:
self._diagnostic_lines.append(
"warning: Function 角色缺少 "
"vefaas:UpdateFunctionResource;本次 Studio 更新继续发布,"
"但 MinInstance 保持原值。请执行一次 `veadk studio update` "
"或在 IAM 中补充该权限。"
)
self._submitted_version = manifest.version
self._set_progress("publishing", "已提交,正在等待新 Revision 发布")
return manifest
Expand Down
Loading
Loading