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 frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -823,6 +823,13 @@ run `npm ci` from `frontend/` to synchronize dependencies with the lockfile
before rebuilding. Reusing another checkout's `node_modules` can retain older
dependencies even when the current `package.json` already declares them

### Studio deployment resources

`veadk studio deploy` configures the Studio function with 8 vCPU, 16 GB memory,
and both minimum and maximum instance counts set to 1 on Volcengine and BytePlus
These settings apply to new deployments and redeployments of an existing Studio
Ordinary Agent deployments retain their existing resource defaults

### Identity-backed user management

Deploy with `--super-admin <existing-user-email-or-uid>` to select the first
Expand Down
19 changes: 19 additions & 0 deletions frontend/server/studio_deployment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# 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.

"""Default function resources for Studio on Volcengine and BytePlus"""

STUDIO_CPU_MILLI = 8000
STUDIO_MEMORY_MB = 16384
STUDIO_MAX_INSTANCE = 1
9 changes: 9 additions & 0 deletions tests/cli/test_studio_deploy_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -1040,6 +1040,7 @@ def __init__(self, **kwargs: object) -> None:
captured.update(kwargs)

def deploy(self, **kwargs: object) -> SimpleNamespace:
captured["deployment"] = kwargs
return SimpleNamespace(
vefaas_endpoint="https://studio.example.com",
vefaas_application_id="app-id",
Expand Down Expand Up @@ -1122,6 +1123,11 @@ def configure_user_pool_for_idp_only(self, user_pool_uid: str) -> None:
assert result.exit_code == 0, result.output
assert captured["region"] == expected_region
assert captured["project"] == expected_project
deployment = captured["deployment"]
assert isinstance(deployment, dict)
assert deployment["cpu_milli"] == 8000
assert deployment["memory_mb"] == 16384
assert deployment["max_instance"] == 1
assert captured["volcengine_session_token"] == "sts-token"
assert captured["identity_client"] == {
"access_key": "ak",
Expand Down Expand Up @@ -1504,6 +1510,9 @@ def _resolve_account_id(**kwargs: object) -> str:
assert identity["enable_vefaas_iam_fallback"] is False
deploy = captured["deploy"]
assert isinstance(deploy, dict)
assert deploy["cpu_milli"] == 8000
assert deploy["memory_mb"] == 16384
assert deploy["max_instance"] == 1
assert deploy["enable_mcp_session"] is False
assert deploy["disable_gateway_cors"] is True
assert "--provider byteplus --auth-mode frontend" in str(captured["run_script"])
Expand Down
82 changes: 81 additions & 1 deletion tests/test_cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from veadk.integrations.ve_code_pipeline.ve_code_pipeline import VeCodePipeline
from veadk.integrations.ve_faas.ve_faas import VeFaaS
from veadk.utils.cloud_provider import (
CloudProvider,
agentkit_openapi_base,
apmplus_otlp_endpoint,
cp_openapi_host,
Expand Down Expand Up @@ -66,6 +67,80 @@ def test_vefaas_create_function_uses_configured_project() -> None:

request = service.client.create_function.call_args.args[0]
assert request.project_name == "studio-project"
assert request.cpu_milli is None
assert request.memory_mb == 2048


@pytest.mark.parametrize("provider", ["volcengine", "byteplus"])
@pytest.mark.parametrize("existing", [False, True])
@pytest.mark.parametrize("studio_resources", [False, True])
def test_cloud_deploy_function_resources(
provider: CloudProvider, existing: bool, studio_resources: bool, tmp_path: Path
) -> None:
engine = CloudAgentEngine(
volcengine_access_key="test_access_key",
volcengine_secret_key="test_secret_key",
provider=provider,
region="ap-southeast-1" if provider == "byteplus" else "cn-beijing",
)
service = engine._vefaas_service
service.client = Mock()
service.client.create_function.return_value = SimpleNamespace(
id="function-id", project_name="default"
)
service.client.get_function.return_value = SimpleNamespace(
command="bash ./run.sh", envs=[]
)
service.find_app_id_by_name = Mock(
return_value="application-id" if existing else None
)
service._get_application_status = Mock(
return_value=(
"deploy_success",
{
"Result": {
"CloudResource": '{"framework":{"function":'
'{"Id":"function-id","Name":"studio-app-fn"}}}'
}
},
)
)
service._upload_and_mount_code = Mock()
service._create_application = Mock(return_value="application-id")
service._release_application = Mock(return_value="https://studio.example")
service.ensure_application_route_methods = Mock()
service.get_application_route = Mock(return_value=("gateway", "service", "route"))

with (
patch("veadk.cloud.cloud_agent_engine.CloudApp"),
patch.dict("veadk.config.veadk_environments", {}, clear=True),
):
engine.deploy(
"studio-app",
str(tmp_path),
gateway_name="gateway",
gateway_service_name="service",
gateway_upstream_name="upstream",
cpu_milli=8000 if studio_resources else None,
memory_mb=16384 if studio_resources else None,
max_instance=1 if studio_resources else None,
)

operation = (
service.client.update_function if existing else service.client.create_function
)
operation.assert_called_once()
request = operation.call_args.args[0]
assert request.cpu_milli == (8000 if studio_resources else None)
assert request.memory_mb == (
16384 if studio_resources else None if existing else 2048
)
service.client.update_function_resource.assert_called_once()
resources = service.client.update_function_resource.call_args.args[0]
assert resources.function_id == "function-id"
assert resources.min_instance == 1
assert resources.max_instance == (1 if studio_resources else None)
assert resources.reserved_frozen_instance is None


@pytest.mark.parametrize("provider", ["volcengine", "byteplus"])
Expand Down Expand Up @@ -230,6 +305,9 @@ def test_vefaas_deploy_updates_existing_application_in_place() -> None:
environment_overrides={"VEADK_STUDIO_DEPLOY_ID": "deploy-id"},
disable_gateway_cors=True,
normalize_studio_entrypoint=True,
cpu_milli=None,
memory_mb=None,
max_instance=None,
)
service._create_function.assert_not_called()
service._create_application.assert_not_called()
Expand Down Expand Up @@ -389,7 +467,9 @@ def test_vefaas_deploy_can_disable_gateway_cors() -> None:
"application-id",
disable_cors=True,
)
service._set_function_min_instance.assert_called_once_with("function-id")
service._set_function_min_instance.assert_called_once_with(
"function-id", max_instance=None
)


def test_vefaas_code_upload_callback_uses_configured_region() -> None:
Expand Down
8 changes: 8 additions & 0 deletions veadk/cli/cli_frontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -16538,6 +16538,11 @@ def frontend_deploy(

# 3) Deploy the function + a plain public APIG trigger on the serverless
# gateway (auth_method="none" — no gateway SSO plugin / domain upstream).
from frontend.server.studio_deployment import (
STUDIO_CPU_MILLI,
STUDIO_MAX_INSTANCE,
STUDIO_MEMORY_MB,
)
from veadk.cloud.cloud_agent_engine import CloudAgentEngine

engine = CloudAgentEngine(
Expand All @@ -16564,6 +16569,9 @@ def frontend_deploy(
enable_mcp_session=False,
keep_failed_deploy=keep_failed_deploy,
disable_gateway_cors=True,
cpu_milli=STUDIO_CPU_MILLI,
memory_mb=STUDIO_MEMORY_MB,
max_instance=STUDIO_MAX_INSTANCE,
)
url = (app.vefaas_endpoint or "").rstrip("/")
redirect_uri = f"{url}/oauth2/callback"
Expand Down
9 changes: 9 additions & 0 deletions veadk/cloud/cloud_agent_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,9 @@ def deploy(
enable_mcp_session: bool = True,
keep_failed_deploy: bool = False,
disable_gateway_cors: bool = False,
cpu_milli: int | None = None,
memory_mb: int | None = None,
max_instance: int | None = None,
) -> CloudApp:
"""Deploys a local agent project to Volcengine FaaS, creating necessary resources.

Expand All @@ -271,6 +274,9 @@ def deploy(
identity_client_name (str, optional): Custom client name. Defaults to timestamped.
local_test (bool): Perform FastAPI server test before deploy. Defaults to False.
disable_gateway_cors (bool): Disable route-wide APIG CORS. Defaults to False.
cpu_milli: Function CPU in millicores; omitted to use platform defaults.
memory_mb: Function memory in MB; omitted to preserve existing settings.
max_instance: Maximum function instances; omitted to preserve cloud settings.

Returns:
CloudApp: Deployed application with endpoint, name, and ID.
Expand Down Expand Up @@ -334,6 +340,9 @@ def deploy(
enable_mcp_session=enable_mcp_session,
keep_failed_deploy=keep_failed_deploy,
disable_gateway_cors=disable_gateway_cors,
cpu_milli=cpu_milli,
memory_mb=memory_mb,
max_instance=max_instance,
)
_ = function_id # for future use

Expand Down
57 changes: 46 additions & 11 deletions veadk/integrations/ve_faas/ve_faas.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,14 @@ def _upload_and_mount_code(self, function_id: str, path: str):

return res

def _create_function(self, function_name: str, path: str):
def _create_function(
self,
function_name: str,
path: str,
*,
cpu_milli: int | None = None,
memory_mb: int | None = None,
):
# Read envs
envs = []
for key, value in veadk.config.veadk_environments.items():
Expand All @@ -373,7 +380,8 @@ def _create_function(self, function_name: str, path: str):
runtime="native-python3.12/v1",
request_timeout=1800,
envs=envs,
memory_mb=2048,
cpu_milli=cpu_milli,
memory_mb=memory_mb if memory_mb is not None else 2048,
role=getenv("IAM_ROLE", None, allow_false_values=True),
project_name=self.project_name,
)
Expand Down Expand Up @@ -590,13 +598,15 @@ def update_application_code_bundle(
environment_overrides: dict[str, str] | None = None,
disable_gateway_cors: bool = False,
normalize_studio_entrypoint: bool = False,
cpu_milli: int | None = None,
memory_mb: int | None = None,
max_instance: int | None = None,
) -> str:
"""Replace an application's function bundle and release it.

Existing function settings are left untouched except that the minimum
instance count is set to one. When environment overrides are provided,
they are merged with the complete current environment before updating the
function.
Existing resource settings are preserved unless explicitly overridden;
the minimum instance count is set to one. Environment overrides are merged
with the complete current environment before updating the function.

Args:
application_id: Existing VeFaaS Application ID.
Expand All @@ -608,6 +618,9 @@ def update_application_code_bundle(
command with ``bash ./run.sh`` so platform archive mode
normalization cannot prevent Studio from starting. Custom
commands are preserved.
cpu_milli: Explicit function CPU override in millicores.
memory_mb: Explicit function memory override in MB.
max_instance: Explicit maximum instance count override.

Returns:
The existing Application URL after the new revision is released.
Expand All @@ -617,9 +630,11 @@ def update_application_code_bundle(
path=path,
environment_overrides=environment_overrides,
normalize_studio_entrypoint=normalize_studio_entrypoint,
cpu_milli=cpu_milli,
memory_mb=memory_mb,
)
url = self._release_application(application_id)
self._set_function_min_instance(function_id)
self._set_function_min_instance(function_id, max_instance=max_instance)
if disable_gateway_cors:
self.ensure_application_route_methods(
application_id,
Expand Down Expand Up @@ -661,9 +676,15 @@ def _replace_application_code_bundle(
environment_overrides: dict[str, str] | None,
request_timeout: int | None = None,
normalize_studio_entrypoint: bool = False,
cpu_milli: int | None = None,
memory_mb: int | None = None,
) -> None:
"""Upload a bundle and update the Function without releasing it."""
request_options: dict[str, Any] = {"id": function_id}
if cpu_milli is not None:
request_options["cpu_milli"] = cpu_milli
if memory_mb is not None:
request_options["memory_mb"] = memory_mb
if request_timeout is not None:
request_options["request_timeout"] = request_timeout
function: Any | None = None
Expand Down Expand Up @@ -694,12 +715,15 @@ def _replace_application_code_bundle(
volcenginesdkvefaas.UpdateFunctionRequest(**request_options)
)

def _set_function_min_instance(self, function_id: str) -> None:
"""Set only a Function's minimum instance count to one."""
def _set_function_min_instance(
self, function_id: str, *, max_instance: int | None = None
) -> None:
"""Keep one warm instance and optionally set the maximum instance count"""
self.client.update_function_resource(
volcenginesdkvefaas.UpdateFunctionResourceRequest(
function_id=function_id,
min_instance=1,
max_instance=max_instance,
)
)

Expand Down Expand Up @@ -1016,6 +1040,9 @@ def deploy(
enable_mcp_session: bool = True,
keep_failed_deploy: bool = False,
disable_gateway_cors: bool = False,
cpu_milli: int | None = None,
memory_mb: int | None = None,
max_instance: int | None = None,
) -> tuple[str, str, str]:
"""Deploy an agent project to VeFaaS service.

Expand All @@ -1027,6 +1054,9 @@ def deploy(
gateway_upstream_name (str, optional): Gateway upstream name. Defaults to "".
enable_key_auth (bool, optional): Enable key auth. Defaults to False.
disable_gateway_cors (bool, optional): Disable route-wide APIG CORS.
cpu_milli: Function CPU override in millicores.
memory_mb: Function memory override in MB; new functions default to 2048.
max_instance: Maximum instance override; omitted to preserve cloud settings.

Returns:
tuple[str, str, str]: (url, app_id, function_id)
Expand Down Expand Up @@ -1068,6 +1098,9 @@ def deploy(
},
disable_gateway_cors=disable_gateway_cors,
normalize_studio_entrypoint=True,
cpu_milli=cpu_milli,
memory_mb=memory_mb,
max_instance=max_instance,
)
logger.info(
f"VeFaaS application {name} with ID {existing_app_id} updated on {url}."
Expand Down Expand Up @@ -1103,7 +1136,9 @@ def deploy(
f"Start to create VeFaaS function {function_name} with path {path}. Gateway: {gateway_name}, Gateway Service: {gateway_service_name}, Gateway Upstream: {gateway_upstream_name}."
)
try:
function_name, function_id = self._create_function(function_name, path)
function_name, function_id = self._create_function(
function_name, path, cpu_milli=cpu_milli, memory_mb=memory_mb
)
logger.info(
f"VeFaaS function {function_name} with ID {function_id} created."
)
Expand All @@ -1122,7 +1157,7 @@ def deploy(
logger.info(f"VeFaaS application {name} with ID {app_id} created.")
logger.info(f"Start to release VeFaaS application {app_id}.")
url = self._release_application(app_id)
self._set_function_min_instance(function_id)
self._set_function_min_instance(function_id, max_instance=max_instance)
self.ensure_application_route_methods(
app_id,
disable_cors=disable_gateway_cors,
Expand Down
Loading