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
3 changes: 3 additions & 0 deletions frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,9 @@ When an update fails, the administrator dialog shows the failed stage, a
searchable error ID, the complete diagnostic timeline and exception chain, and
a direct link to the deployed Function in the VeFaaS console. The log can be
copied in full for support, and retrying starts a fresh diagnostic record.
Reading the VeFaaS release log is optional: when the Function role lacks
`vefaas:GetApplicationRevisionLog`, the update continues and the dialog links
to the matching provider IAM console so an administrator can grant access.

`.github/workflows/publish-studio-release.yaml` runs only when it is manually
dispatched on `main`. Enter the user-facing changelog when starting the
Expand Down
15 changes: 3 additions & 12 deletions frontend/server/studio_update_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
from typing import Any, Literal

from frontend.server.storage.provisioning import resolve_studio_storage_for_deploy
from veadk.cli.frontend_deploy_iam import ensure_default_frontend_role_policy
from veadk.utils.cloud_provider import CloudProvider

SnapshotKind = Literal["codex", "openclaw", "hermes"]
Expand All @@ -35,7 +34,7 @@
def _function_config(
function_client: Any,
function_id: str,
) -> tuple[dict[str, str], str]:
) -> dict[str, str]:
import volcenginesdkvefaas

function = function_client.get_function(
Expand All @@ -46,7 +45,7 @@ def _function_config(
for item in (getattr(function, "envs", None) or [])
if getattr(item, "key", None)
}
return environment, str(getattr(function, "role", "") or "").strip()
return environment


def _provision_snapshot_tool(
Expand Down Expand Up @@ -136,7 +135,7 @@ def reconcile_studio_update_resources(
session_token: str,
) -> dict[str, str]:
"""Return environment overrides for resources missing from an older Studio."""
environment, function_role = _function_config(function_client, function_id)
environment = _function_config(function_client, function_id)
overrides: dict[str, str] = {}

from veadk.cli.studio_knowledge_signing import (
Expand All @@ -149,14 +148,6 @@ def reconcile_studio_update_resources(
resolve_studio_knowledge_signing_key(environment)
)

ensure_default_frontend_role_policy(
function_role,
access_key=access_key,
secret_key=secret_key,
session_token=session_token,
provider=provider,
)

if not (
environment.get("VEADK_STUDIO_TOS_BUCKET")
and environment.get("VEADK_STUDIO_TOS_REGION")
Expand Down
1 change: 1 addition & 0 deletions frontend/src/adk/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2615,6 +2615,7 @@ export interface StudioUpdateStatus {
updateLogs: string[];
updateLogsVisible: boolean;
consoleUrl: string;
permissionConsoleUrl: string;
}

/** Check the configured immutable Studio main release channel. */
Expand Down
51 changes: 51 additions & 0 deletions frontend/src/ui/StudioUpdateControl.css
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,57 @@
text-underline-offset: 2px;
}

.studio-update-console-link svg,
.studio-update-permission-notice svg {
width: 14px;
height: 14px;
flex: 0 0 14px;
stroke: currentColor;
stroke-width: 1.5;
stroke-linecap: round;
stroke-linejoin: round;
}

.studio-update-permission-notice {
display: grid;
align-content: center;
gap: 8px;
min-height: 92px;
padding: 12px;
border: 1px solid hsl(var(--border));
border-radius: 8px;
background: hsl(var(--muted) / 0.22);
color: hsl(var(--muted-foreground));
font-size: 11px;
line-height: 1.55;
}

.studio-update-permission-notice p {
margin: 0;
}

.studio-update-permission-notice code {
margin: 0 3px;
color: hsl(var(--foreground));
font-family: inherit;
font-weight: 500;
}

.studio-update-permission-notice a {
display: inline-flex;
align-items: center;
gap: 5px;
width: fit-content;
color: hsl(var(--primary));
font-weight: 500;
text-decoration: none;
}

.studio-update-permission-notice a:hover {
text-decoration: underline;
text-underline-offset: 2px;
}

.studio-update-progress-summary {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
Expand Down
38 changes: 37 additions & 1 deletion frontend/src/ui/StudioUpdateControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,31 @@ function VersionCheckIcon() {
);
}

function ExternalLinkIcon() {
return (
<svg viewBox="0 0 16 16" fill="none" aria-hidden>
<path d="M6.25 3.75H3.5v8.75h8.75V9.75" />
<path d="M8.5 3.5h4v4" />
<path d="m7.25 8.75 5-5" />
</svg>
);
}

function StudioUpdateLogPermissionNotice({ href }: { href: string }) {
return (
<div className="studio-update-permission-notice" role="status">
<p>
无法读取 VeFaaS 发布日志。Function 角色缺少
<code>vefaas:GetApplicationRevisionLog</code> 权限,更新会继续。
</p>
<a href={href} target="_blank" rel="noreferrer">
前往 IAM 控制台配置权限
<ExternalLinkIcon />
</a>
</div>
);
}

function StudioUpdateLog({
lines,
phase,
Expand Down Expand Up @@ -287,6 +312,7 @@ export function StudioUpdateControl({
completionDetectedAtRef.current = now;
}
if (
next.updateLogsVisible !== false &&
!deploymentLogComplete(next.updateLogs) &&
now - completionDetectedAtRef.current < COMPLETION_LOG_SETTLE_TIMEOUT_MS
) {
Expand Down Expand Up @@ -509,6 +535,11 @@ export function StudioUpdateControl({
onCopy={(lines) => void copyUpdateLog(lines)}
/>
)}
{status.updateLogsVisible === false && (
<StudioUpdateLogPermissionNotice
href={status.permissionConsoleUrl}
/>
)}
{status.consoleUrl && (
<a
className="studio-update-console-link"
Expand All @@ -517,7 +548,7 @@ export function StudioUpdateControl({
rel="noreferrer"
>
前往 VeFaaS 控制台查看 Function 日志
<span aria-hidden>↗</span>
<ExternalLinkIcon />
</a>
)}
</div>
Expand Down Expand Up @@ -569,6 +600,11 @@ export function StudioUpdateControl({
onCopy={(lines) => void copyUpdateLog(lines)}
/>
)}
{status.updateLogsVisible === false && (
<StudioUpdateLogPermissionNotice
href={status.permissionConsoleUrl}
/>
)}
<p className="studio-update-progress-note">
发布阶段会短暂中断连接;关闭此窗口不会停止更新,可随时点击右上角按钮重新查看。
</p>
Expand Down
14 changes: 13 additions & 1 deletion frontend/tests/studioUpdate.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ test("update submission is explicit and survives a revision switch", () => {
assert.match(controlSource, /setDialogOpen\(true\)/);
assert.match(controlSource, /COMPLETION_LOG_SETTLE_TIMEOUT_MS = 45_000/);
assert.match(controlSource, /deploymentLogComplete\(next\.updateLogs\)/);
assert.match(
controlSource,
/next\.updateLogsVisible !== false &&[\s\S]*?!deploymentLogComplete\(next\.updateLogs\)/,
);
assert.match(controlSource, /line\.includes\("部署应用成功"\)/);
});

Expand Down Expand Up @@ -185,10 +189,18 @@ test("Studio renders bounded VeFaaS logs without stealing manual scroll", () =>
);
});

test("Studio hides only the log region when VeFaaS log permission is missing", () => {
test("Studio explains how to grant optional VeFaaS log permission", () => {
assert.match(
controlSource,
/\{status\.updateLogsVisible !== false && \([\s\S]*?<StudioUpdateLog/,
);
assert.match(controlSource, /status\.updateLogsVisible !== false/g);
assert.match(controlSource, /status\.updateLogsVisible === false/g);
assert.match(controlSource, /vefaas:GetApplicationRevisionLog/);
assert.match(controlSource, /更新会继续/);
assert.match(controlSource, /前往 IAM 控制台配置权限/);
assert.match(controlSource, /href=\{status\.permissionConsoleUrl\}/);
assert.match(clientSource, /permissionConsoleUrl: string/);
assert.match(controlStyleSource, /studio-update-permission-notice/);
assert.doesNotMatch(controlSource, /↗/);
});
4 changes: 2 additions & 2 deletions tests/cli/test_frontend_deploy_iam.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,8 +263,8 @@ def test_frontend_policy_allows_release_download() -> None:
assert "vefaas:GetCodeUploadAddress" in actions
assert "vefaas:GetApplication" in actions
assert "vefaas:GetApplicationRevisionLog" in actions
assert "iam:CreatePolicy" in actions
assert "iam:UpdatePolicy" in actions
assert "iam:CreatePolicy" not in actions
assert "iam:UpdatePolicy" not in actions
assert "vefaas:CodeUploadCallback" in actions
assert "vefaas:UpdateFunction" in actions
assert "vefaas:ReleaseApplication" in actions
Expand Down
15 changes: 14 additions & 1 deletion tests/cli/test_studio_self_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,17 @@ def test_byteplus_console_url_uses_byteplus_domain() -> None:
"https://console.byteplus.com/vefaas/"
"region:vefaas+ap-southeast-1/function/detail/function-id"
)
assert updater._permission_console_url() == "https://console.byteplus.com/iam"


def test_volcengine_permission_console_url_uses_volcengine_domain() -> None:
updater = StudioSelfUpdater(
settings=_settings(),
credential_resolver=lambda: ("ak", "sk", ""),
branding_logo=None,
)

assert updater._permission_console_url() == "https://console.volcengine.com/iam"


def test_application_status_uses_current_function_environment(
Expand Down Expand Up @@ -607,7 +618,9 @@ def _get_application_logs(self, *_args: Any, **_kwargs: Any) -> list[str]:
monkeypatch.setattr("veadk.integrations.ve_faas.ve_faas.VeFaaS", _VeFaaS)

assert updater._load_vefaas_logs(7) == []
assert updater._progress_payload()["updateLogsVisible"] is False
progress = updater._progress_payload()
assert progress["updateLogsVisible"] is False
assert progress["permissionConsoleUrl"] == "https://console.volcengine.com/iam"


def test_non_permission_log_error_keeps_log_region_visible(
Expand Down
16 changes: 3 additions & 13 deletions tests/frontend/server/test_studio_update_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,14 @@ def _client(
("byteplus", "ap-southeast-1"),
],
)
def test_reconcile_refreshes_the_default_function_role_policy(
def test_reconcile_does_not_mutate_the_function_role_policy(
monkeypatch: pytest.MonkeyPatch,
provider: str,
region: str,
) -> None:
calls: list[dict[str, str]] = []
monkeypatch.setattr(
"frontend.server.studio_update_resources.ensure_default_frontend_role_policy",
lambda role, **kwargs: calls.append({"role": role, **kwargs}) or True,
"veadk.cli.frontend_deploy_iam.ensure_default_frontend_role_policy",
lambda *_args, **_kwargs: pytest.fail("self-update must not mutate IAM"),
)
monkeypatch.setattr(
"frontend.server.studio_update_resources.resolve_studio_storage_for_deploy",
Expand Down Expand Up @@ -89,15 +88,6 @@ def test_reconcile_refreshes_the_default_function_role_policy(
)
== {}
)
assert calls == [
{
"role": "trn:iam::123:role/VeADKFrontendServiceRole",
"access_key": "ak",
"secret_key": "sk",
"session_token": "token",
"provider": provider,
}
]


def test_reconcile_studio_update_resources_reuses_existing_resources(
Expand Down
2 changes: 0 additions & 2 deletions veadk/cli/frontend_deploy_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,6 @@
"ESCloud:ListMLApiKeys",
"iam:AttachRolePolicy",
"iam:CheckServiceLinkedRole",
"iam:CreatePolicy",
"iam:CreateRole",
"iam:CreateServiceLinkedRole",
"iam:DeleteRole",
Expand All @@ -129,7 +128,6 @@
"iam:ListRoles",
"iam:ListUsers",
"iam:UpdateRole",
"iam:UpdatePolicy",
"iam:ListAttachedRolePolicies",
"id:CreateApiKey",
"id:CreateApiKeyCredentialProvider",
Expand Down
10 changes: 10 additions & 0 deletions veadk/cli/studio_self_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,7 @@ def _progress_payload(self) -> dict[str, Any]:
"updateLogs": _tail_log_lines(self._diagnostic_lines),
"updateLogsVisible": self._vefaas_logs_visible,
"consoleUrl": self._console_url(),
"permissionConsoleUrl": self._permission_console_url(),
}

def _status_failure_payload(
Expand Down Expand Up @@ -624,6 +625,15 @@ def _console_url(self) -> str:
f"{self._settings.function_id}"
)

def _permission_console_url(self) -> str:
"""Return the provider IAM console used to grant optional log access."""
console_host = (
"console.byteplus.com"
if self._settings.provider == "byteplus"
else "console.volcengine.com"
)
return f"https://{console_host}/iam"

def _application_status(self) -> tuple[str, str, int, bool]:
"""Read the Application status and Function's configured release version."""
access_key, secret_key, session_token = self._credential_resolver()
Expand Down

Large diffs are not rendered by default.

Loading
Loading