Skip to content
Open
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
24 changes: 4 additions & 20 deletions cli/src/api/cvms.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {
safeGetCvmList,
safeGetCvmInfo,
safeGetCvmComposeFile,
type Client,
type CvmInfoDetailV20260121,
} from "@phala/cloud";
Expand All @@ -20,7 +19,6 @@ import type {
GetCvmNetworkResponse,
TeepodResponse,
PubkeyResponse,
CvmComposeConfigResponse,
UpgradeResponse,
} from "./types";
import inquirer from "inquirer";
Expand All @@ -45,22 +43,6 @@ export async function getCvmByAppId(
return result.data;
}

/**
* Get CVM compose configuration
*/
export async function getCvmComposeConfig(
cvmId: string,
): Promise<CvmComposeConfigResponse> {
const client = await getClient();
const result = await safeGetCvmComposeFile(client, { id: cvmId });

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

return result.data as CvmComposeConfigResponse;
}

/**
* Get CVM network information
* @param appId App ID (with or without app_ prefix)
Expand Down Expand Up @@ -168,11 +150,13 @@ export interface ResizeCvmPayload {
}

/**
* Replicate a CVM
* Replicate a CVM instance
* @param appId App ID (with or without app_ prefix)
* @param vmUuid Source CVM UUID (with or without dashes)
*/
export async function replicateCvm(
appId: string,
vmUuid: string,
payload: {
teepod_id?: number;
encrypted_env?: string;
Expand All @@ -181,7 +165,7 @@ export async function replicateCvm(
const client = await getClient();
const cleanAppId = appId.replace(/^app_/, "");
const response = await client.post<ReplicateCvmResponse>(
`cvms/app_${cleanAppId}/replicas`,
`apps/${cleanAppId}/cvms/${vmUuid}/replicas`,
payload,
);
return replicateCvmResponseSchema.parse(response);
Expand Down
28 changes: 11 additions & 17 deletions cli/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,22 +148,22 @@ export const replicateCvmResponseSchema = z.object({
id: z.number(),
name: z.string(),
}),
user_id: z.number().nullable(),
user_id: z.number().nullable().optional(),
app_id: z.string(),
vm_uuid: z.string().nullable(),
instance_id: z.string().nullable(),
app_url: z.string().nullable(),
base_image: z.string().nullable(),
vm_uuid: z.string().nullable().optional(),
instance_id: z.string().nullable().optional(),
app_url: z.string().nullable().optional(),
base_image: z.string().nullable().optional(),
vcpu: z.number(),
memory: z.number(),
disk_size: z.number(),
manifest_version: z.number().nullable(),
version: z.string().nullable(),
runner: z.string().nullable(),
docker_compose_file: z.string().nullable(),
features: z.array(z.string()).nullable(),
manifest_version: z.number().nullable().optional(),
version: z.string().nullable().optional(),
runner: z.string().nullable().optional(),
docker_compose_file: z.string().nullable().optional(),
features: z.array(z.string()).nullable().optional(),
created_at: z.string(),
encrypted_env_pubkey: z.string().nullable(),
encrypted_env_pubkey: z.string().nullable().optional(),
});

export type ReplicateCvmResponse = z.infer<typeof replicateCvmResponseSchema>;
Expand Down Expand Up @@ -309,9 +309,3 @@ export interface AvailableNodesResponse {
nodes: TEEPod[];
kms_list?: KmsListItem[];
}

// CVM Compose Config Response
export interface CvmComposeConfigResponse {
env_pubkey: string;
[key: string]: unknown;
}
47 changes: 4 additions & 43 deletions cli/src/commands/allow-devices/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,5 @@
import chalk from "chalk";
import inquirer from "inquirer";
import {
type Chain,
type PublicClient,
type WalletClient,
createPublicClient,
createWalletClient,
http,
} from "viem";
import { privateKeyToAccount, nonceManager } from "viem/accounts";
import {
safeGetCvmInfo,
safeGetAppDeviceAllowlist,
Expand Down Expand Up @@ -163,24 +154,6 @@ function resolvePrivateKey(input: { privateKey?: string }): `0x${string}` {
return (key.startsWith("0x") ? key : `0x${key}`) as `0x${string}`;
}

function createSharedClients(
chain: Chain,
privateKey: `0x${string}`,
rpcUrl?: string,
) {
const account = privateKeyToAccount(privateKey, { nonceManager });
const publicClient = createPublicClient({
chain,
transport: http(rpcUrl),
}) as unknown as PublicClient;
const walletClient = createWalletClient({
account,
chain,
transport: http(rpcUrl),
}) as unknown as WalletClient;
return { publicClient, walletClient };
}

async function resolveDeviceIdOrNodeName(
deviceInput: string,
): Promise<`0x${string}`> {
Expand Down Expand Up @@ -429,12 +402,6 @@ async function runAdd(
return 1;
}

const { publicClient, walletClient } = createSharedClients(
chain,
privateKey,
input.rpcUrl,
);

const results: {
deviceId: string;
txHash: string;
Expand All @@ -444,10 +411,10 @@ async function runAdd(
for (const deviceId of deviceIds) {
const result = await safeAddDevice({
chain,
rpcUrl: input.rpcUrl,
appAddress: appContractAddress,
deviceId,
walletClient,
publicClient,
privateKey,
skipPrerequisiteChecks: true,
});

Expand Down Expand Up @@ -574,12 +541,6 @@ async function runRemove(
return 1;
}

const { publicClient, walletClient } = createSharedClients(
chain,
privateKey,
input.rpcUrl,
);

const results: {
deviceId: string;
txHash: string;
Expand All @@ -589,10 +550,10 @@ async function runRemove(
for (const deviceId of deviceIds) {
const result = await safeRemoveDevice({
chain,
rpcUrl: input.rpcUrl,
appAddress: appContractAddress,
deviceId,
walletClient,
publicClient,
privateKey,
skipPrerequisiteChecks: true,
});

Expand Down
104 changes: 74 additions & 30 deletions cli/src/commands/cvms/replicate/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
import fs from "node:fs";
import path from "node:path";
import { CvmIdSchema, encryptEnvVars } from "@phala/cloud";
import { getCvmComposeConfig, replicateCvm } from "@/src/api/cvms";
import {
type PhalaCloudError,
ResourceError,
formatErrorMessage,
formatStructuredError,
safeGetCvmInfo,
safeGetCurrentUser,
encryptEnvVars,
} from "@phala/cloud";
import { replicateCvm } from "@/src/api/cvms";
import { getClient } from "@/src/lib/client";
import { getEncryptPubkey } from "@/src/commands/envs/get-encrypt-pubkey";
import { defineCommand } from "@/src/core/define-command";
import { isInJsonMode } from "@/src/core/json-mode";
import type { CommandContext } from "@/src/core/types";

import { CLOUD_URL } from "@/src/utils/constants";
import { logger } from "@/src/utils/logger";
import {
cvmsReplicateCommandMeta,
Expand Down Expand Up @@ -38,8 +50,20 @@ async function runCvmsReplicateCommand(
return 1;
}

const { cvmId: normalizedCvmId } = CvmIdSchema.parse(context.cvmId);
let encryptedEnv: string | undefined;
const client = await getClient(context);
const [cvmResult, currentUserResult] = await Promise.all([
safeGetCvmInfo(client, context.cvmId),
safeGetCurrentUser(client),
]);
if (!cvmResult.success) {
throw new Error(cvmResult.error.message);
}
if (!currentUserResult.success) {
throw new Error(currentUserResult.error.message);
}
const sourceCvm = cvmResult.data;
const workspace = currentUserResult.data.workspace;

if (input.envFile) {
const envPath = path.resolve(process.cwd(), input.envFile);
Expand All @@ -48,10 +72,8 @@ async function runCvmsReplicateCommand(
}

const envVars = parseEnvFile(envPath);
const cvmConfig = await getCvmComposeConfig(normalizedCvmId);

logger.info("Encrypting environment variables...");
encryptedEnv = await encryptEnvVars(envVars, cvmConfig.env_pubkey);
const pubkey = await getEncryptPubkey(client, sourceCvm);
encryptedEnv = await encryptEnvVars(envVars, pubkey);
}

const requestBody: { teepod_id?: number; encrypted_env?: string } = {};
Expand All @@ -63,36 +85,58 @@ async function runCvmsReplicateCommand(
requestBody.encrypted_env = encryptedEnv;
}

const replica = await replicateCvm(normalizedCvmId, requestBody);
if (!sourceCvm.vm_uuid) {
throw new Error("Source CVM has no vm_uuid");
}

logger.success(
`Successfully created replica of CVM UUID: ${normalizedCvmId} with App ID: ${replica.app_id}`,
const replica = await replicateCvm(
sourceCvm.app_id,
sourceCvm.vm_uuid,
requestBody,
);

logger.keyValueTable(
{
"CVM UUID": replica.vm_uuid.replace(/-/g, ""),
"App ID": replica.app_id,
Name: replica.name,
Status: replica.status,
TEEPod: `${replica.teepod.name} (ID: ${replica.teepod_id})`,
vCPUs: replica.vcpu,
Memory: `${replica.memory} MB`,
"Disk Size": `${replica.disk_size} GB`,
"App URL":
replica.app_url ||
`${process.env.CLOUD_URL || "https://cloud.phala.com"}/dashboard/cvms/${replica.vm_uuid.replace(/-/g, "")}`,
},
{ borderStyle: "rounded" },
);
if (isInJsonMode()) {
context.success(replica);
return 0;
}

logger.success(
`Your CVM replica is being created. You can check its status with:
phala cvms get ${replica.app_id}`,
);
const vmUuid = replica.vm_uuid?.replace(/-/g, "") ?? "";
const teamLabel =
workspace.slug && workspace.slug !== workspace.name
? `${workspace.name} (${workspace.slug})`
: workspace.slug || workspace.name;
const appUrl =
workspace.slug && vmUuid
? `${CLOUD_URL}/${workspace.slug}/apps/${replica.app_id}/instances/${vmUuid}`
: replica.app_url || "-";
const lines = [
`Source CVM ID: ${context.cvmId.id}`,
`Team: ${teamLabel}`,
`CVM UUID: ${vmUuid || "-"}`,
`App ID: ${replica.app_id}`,
`Name: ${replica.name}`,
`Status: ${replica.status}`,
`TEEPod: ${replica.teepod.name} (ID: ${replica.teepod_id})`,
`vCPUs: ${replica.vcpu}`,
`Memory: ${replica.memory} MB`,
`Disk Size: ${replica.disk_size} GB`,
`App URL: ${appUrl}`,
];
context.stdout.write(`${lines.join("\n")}\n`);
return 0;
} catch (error) {
logger.error("Failed to create CVM replica");
if (error instanceof ResourceError) {
process.stderr.write(`${formatStructuredError(error)}\n`);
process.stderr.write(
"Reference the error code above in the handbook for remediation details.\n",
);
return 1;
}
if (error instanceof Error) {
process.stderr.write(`${formatErrorMessage(error as PhalaCloudError)}\n`);
return 1;
}
logger.logDetailedError(error);
return 1;
}
Expand Down
26 changes: 26 additions & 0 deletions cli/src/commands/profiles/delete/command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { z } from "zod";
import type { CommandMeta } from "@/src/core/types";

export const profilesDeleteCommandMeta: CommandMeta = {
name: "delete",
category: "profile",
description: "Delete a profile",
stability: "stable",
arguments: [
{
name: "profile-name",
description: "Profile name",
required: true,
target: "profileName",
},
],
options: [],
};

export const profilesDeleteCommandSchema = z.object({
profileName: z.string().min(1),
});

export type ProfilesDeleteCommandInput = z.infer<
typeof profilesDeleteCommandSchema
>;
Loading
Loading