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
4 changes: 4 additions & 0 deletions cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ phala logs

# SSH into the CVM
phala ssh

# List all replicas for one app, then target one by UUID
phala cvms list --app app_abc123
phala cvms restart 550e8400-e29b-41d4-a716-446655440000
```

> **Tip:** Run `phala link` after your first deploy. It creates a `phala.toml` that binds the directory to the CVM, so subsequent commands (`deploy`, `logs`, `ssh`, `cp`, `ps`) work without specifying a CVM ID. `phala.toml` is safe to commit to version control.
Expand Down
3 changes: 3 additions & 0 deletions cli/docs/cvms.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ List CVMs in your workspace with filtering and pagination.

| Flag | Default | Description |
|------|---------|-------------|
| `--app <app-id>` | | List all CVMs belonging to a specific app |
| `--page` | 1 | Page number for pagination |
| `--page-size` | 30 | Number of items per page |
| `--search <query>` | | Search CVMs by name or ID |
Expand All @@ -36,6 +37,7 @@ List CVMs in your workspace with filtering and pagination.
#### Examples

$ phala cvms list
$ phala cvms list --app app_abc123
$ phala cvms ls --status running --status starting
$ phala cvms list --search my-app --json
$ phala cvms ls --region us-west --page 2
Expand Down Expand Up @@ -67,6 +69,7 @@ Get detailed information about a specific CVM.

$ phala cvms get
$ phala cvms get app_abc123
$ phala cvms get 550e8400-e29b-41d4-a716-446655440000
$ phala cvms get --json
$ phala cvms get --interactive

Expand Down
2 changes: 2 additions & 0 deletions cli/src/commands/cvms/get/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ async function runCvmsGetCommand(
logger.keyValueTable({
Name: cvm.name,
"App ID": `app_${cvm.app_id}`,
"VM UUID": cvm.vm_uuid ?? "N/A",
"Instance ID": cvm.instance_id ?? "N/A",
Status: statusColour,
vCPU: cvm.resource.vcpu,
Memory:
Expand Down
12 changes: 12 additions & 0 deletions cli/src/commands/cvms/list/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ export const cvmsListCommandMeta: CommandMeta = {
description: "List CVMs",
stability: "unstable",
options: [
{
name: "app",
description: "List all CVMs belonging to a specific app_id",
type: "string",
target: "appId",
group: "basic",
},
{
name: "page",
description: "Page number (1-based)",
Expand Down Expand Up @@ -86,6 +93,10 @@ export const cvmsListCommandMeta: CommandMeta = {
name: "List CVMs",
value: "phala cvms ls",
},
{
name: "List all replicas for an app",
value: "phala cvms ls --app app_123",
},
{
name: "Second page",
value: "phala cvms ls --page 2",
Expand All @@ -106,6 +117,7 @@ export const cvmsListCommandMeta: CommandMeta = {
};

export const cvmsListCommandSchema = z.object({
appId: z.string().optional(),
page: z.coerce.number().int().min(1).default(1),
pageSize: z.coerce.number().int().min(1).max(100).default(50),
search: z.string().optional(),
Expand Down
255 changes: 235 additions & 20 deletions cli/src/commands/cvms/list/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import chalk from "chalk";
import { safeGetAppCvms, safeGetCvmStatusBatch } from "@phala/cloud";
import { defineCommand } from "@/src/core/define-command";
import type { CommandContext } from "@/src/core/types";
import { getClient } from "@/src/lib/client";
Expand All @@ -12,47 +13,261 @@ import {
type CvmsListCommandInput,
} from "./command";

interface AppScopedListResult {
page: number;
pageSize: number;
total: number;
totalPages: number;
items: Array<{
appId: string;
vmUuid?: string | null;
instanceId?: string | null;
cvmName: string;
status: string;
uptime?: string | null;
}>;
}

function formatStatus(status: string): string {
if (status.toLowerCase().endsWith("ing")) return chalk.yellow(status);
if (status === "running") return chalk.green(status);
if (status === "stopped") return chalk.red(status);
return chalk.yellow(status);
}

function getAppScopedVmUuid(cvm: Record<string, unknown>): string | null {
if (typeof cvm.vm_uuid === "string" && cvm.vm_uuid.length > 0) {
return cvm.vm_uuid;
}

const hosted = cvm.hosted;
if (
hosted &&
typeof hosted === "object" &&
"id" in hosted &&
typeof hosted.id === "string" &&
hosted.id.length > 0
) {
return hosted.id;
}

return null;
}

function getAppScopedInstanceId(cvm: Record<string, unknown>): string | null {
if (typeof cvm.instance_id === "string" && cvm.instance_id.length > 0) {
return cvm.instance_id;
}

const hosted = cvm.hosted;
if (
hosted &&
typeof hosted === "object" &&
"instance_id" in hosted &&
typeof hosted.instance_id === "string" &&
hosted.instance_id.length > 0
) {
return hosted.instance_id;
}

return null;
}

function getAppScopedAppId(cvm: Record<string, unknown>): string | null {
if (typeof cvm.app_id === "string" && cvm.app_id.length > 0) {
return cvm.app_id;
}

const hosted = cvm.hosted;
if (
hosted &&
typeof hosted === "object" &&
"app_id" in hosted &&
typeof hosted.app_id === "string" &&
hosted.app_id.length > 0
) {
return hosted.app_id;
}

return null;
}

function matchesAppScopedFilters(
cvm: Record<string, unknown>,
input: CvmsListCommandInput,
): boolean {
if (input.search) {
const needle = input.search.toLowerCase();
const haystacks = [
typeof cvm.name === "string" ? cvm.name : "",
getAppScopedAppId(cvm) ?? "",
getAppScopedVmUuid(cvm) ?? "",
getAppScopedInstanceId(cvm) ?? "",
];
if (!haystacks.some((value) => value.toLowerCase().includes(needle))) {
return false;
}
}

if (
input.status &&
input.status.length > 0 &&
(typeof cvm.status !== "string" || !input.status.includes(cvm.status))
) {
return false;
}

if (input.listed !== undefined && Boolean(cvm.listed) !== input.listed) {
return false;
}

if (
input.baseImage &&
(!cvm.os ||
typeof cvm.os !== "object" ||
!("name" in cvm.os) ||
cvm.os.name !== input.baseImage)
) {
return false;
}

if (
input.instanceType &&
(!cvm.resource ||
typeof cvm.resource !== "object" ||
!("instance_type" in cvm.resource) ||
cvm.resource.instance_type !== input.instanceType)
) {
return false;
}

if (input.kmsType && cvm.kms_type !== input.kmsType) {
return false;
}

if (
input.node &&
(!cvm.node_info ||
typeof cvm.node_info !== "object" ||
!("name" in cvm.node_info) ||
cvm.node_info.name !== input.node)
) {
return false;
}

if (
input.region &&
!((cvm.node_info &&
typeof cvm.node_info === "object" &&
"region" in cvm.node_info &&
cvm.node_info.region === input.region) ||
(cvm.node &&
typeof cvm.node === "object" &&
"region_identifier" in cvm.node &&
cvm.node.region_identifier === input.region))
) {
return false;
}

return true;
}

async function listCvmsForApp(
input: CvmsListCommandInput,
): Promise<AppScopedListResult> {
const client = await getClient();
const appId = input.appId?.replace(/^app_/, "");
if (!appId) {
throw new Error("App ID is required");
}

const cvmsResult = await safeGetAppCvms(client as never, { appId });
if (!cvmsResult.success) {
throw new Error(cvmsResult.error.message);
}

const filtered = cvmsResult.data.filter((cvm) =>
matchesAppScopedFilters(cvm as Record<string, unknown>, input),
);

const vmUuids = filtered
.map((cvm) => getAppScopedVmUuid(cvm as Record<string, unknown>))
.filter(
(uuid): uuid is string => typeof uuid === "string" && uuid.length > 0,
);

const statusBatch =
vmUuids.length > 0
? await safeGetCvmStatusBatch(client as never, { vmUuids })
: { success: true as const, data: {} };

if (!statusBatch.success) {
throw new Error(statusBatch.error.message);
}

const start = (input.page - 1) * input.pageSize;
const paged = filtered.slice(start, start + input.pageSize);

return {
page: input.page,
pageSize: input.pageSize,
total: filtered.length,
totalPages: filtered.length === 0 ? 1 : Math.ceil(filtered.length / input.pageSize),
items: paged.map((cvm) => {
const normalized = cvm as Record<string, unknown>;
const vmUuid = getAppScopedVmUuid(normalized);
const batch = vmUuid ? statusBatch.data[vmUuid] : undefined;
return {
appId: getAppScopedAppId(normalized) ?? `app_${appId}`,
vmUuid,
instanceId: getAppScopedInstanceId(normalized),
cvmName: cvm.name,
status: batch?.status ?? cvm.status,
uptime: batch?.uptime,
};
}),
};
}

async function runCvmsListCommand(
input: CvmsListCommandInput,
context: CommandContext,
): Promise<number> {
try {
const client = await getClient();
const result = await listAppsWithCvmStatus(client as never, {
page: input.page,
pageSize: input.pageSize,
search: input.search,
status: input.status,
listed: input.listed,
baseImage: input.baseImage,
instanceType: input.instanceType,
kmsType: input.kmsType,
node: input.node,
region: input.region,
});

if (result.success === false) {
context.fail(result.error.message);
return 1;
}
const data = input.appId
? await listCvmsForApp(input)
: await (async () => {
const client = await getClient();
const result = await listAppsWithCvmStatus(client as never, {
page: input.page,
pageSize: input.pageSize,
search: input.search,
status: input.status,
listed: input.listed,
baseImage: input.baseImage,
instanceType: input.instanceType,
kmsType: input.kmsType,
node: input.node,
region: input.region,
});

if (result.success === false) {
throw new Error(result.error.message);
}

const data = result.data;
return result.data;
})();

if (input.json) {
context.success(data);
return 0;
}

const columns = ["APP_ID", "CVM", "STATUS", "UPTIME"] as const;
const columns = ["APP_ID", "VM_UUID", "INSTANCE_ID", "CVM", "STATUS", "UPTIME"] as const;
const rows = data.items.map((item) => ({
APP_ID: item.appId,
VM_UUID: item.vmUuid ?? "-",
INSTANCE_ID: item.instanceId ?? "-",
CVM: item.cvmName,
STATUS: formatStatus(item.status),
UPTIME: item.uptime ?? "-",
Expand Down
4 changes: 4 additions & 0 deletions cli/src/lib/apps/list-apps-with-cvm-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export interface AppsListWithStatusOptions {

export interface AppCvmRow {
readonly appId: string;
readonly vmUuid?: string | null;
readonly instanceId?: string | null;
readonly cvmName: string;
readonly status: string;
readonly uptime?: string | null;
Expand Down Expand Up @@ -112,6 +114,8 @@ export async function listAppsWithCvmStatus(

rows.push({
appId: app.app_id,
vmUuid: currentCvm.vm_uuid ?? null,
instanceId: currentCvm.instance_id ?? null,
cvmName: currentCvm.name,
status,
uptime: batch?.uptime,
Expand Down
Loading
Loading