diff --git a/apps/deploy-web/src/components/deployments/ConfidentialComputeResources.tsx b/apps/deploy-web/src/components/deployments/ConfidentialComputeResources.tsx index 933b98918..4d7462530 100644 --- a/apps/deploy-web/src/components/deployments/ConfidentialComputeResources.tsx +++ b/apps/deploy-web/src/components/deployments/ConfidentialComputeResources.tsx @@ -31,9 +31,10 @@ const formatUnitLabel = (carveout: TeeResourceCarveout) => { function ResourceLine({ label, cpu, memory }: { label: string; cpu: number; memory: number }) { return ( -
+
{label} - + {/* Keep each value intact (e.g. "0.01 CPU") and pinned right so a wrapping label can't break the numbers across lines. */} + {formatCpu(cpu)} {formatMemory(memory)} @@ -50,7 +51,10 @@ export function ConfidentialComputeResources({ carveouts, dependencies: d = DEPE
Confidential compute resources - +
diff --git a/apps/deploy-web/src/components/deployments/ConfigureDeployment/ConfigurationPane/ConfidentialComputeCard/ConfidentialComputeCard.spec.tsx b/apps/deploy-web/src/components/deployments/ConfigureDeployment/ConfigurationPane/ConfidentialComputeCard/ConfidentialComputeCard.spec.tsx index a68c0f094..3a5e1d5fe 100644 --- a/apps/deploy-web/src/components/deployments/ConfigureDeployment/ConfigurationPane/ConfidentialComputeCard/ConfidentialComputeCard.spec.tsx +++ b/apps/deploy-web/src/components/deployments/ConfigureDeployment/ConfigurationPane/ConfidentialComputeCard/ConfidentialComputeCard.spec.tsx @@ -1,5 +1,6 @@ import type { PropsWithChildren } from "react"; import { FormProvider, useForm } from "react-hook-form"; +import { TooltipProvider } from "@akashnetwork/ui/components"; import { describe, expect, it } from "vitest"; import type { SdlBuilderFormValuesType, ServiceType } from "@src/types"; @@ -118,6 +119,38 @@ describe(ConfidentialComputeCard.name, () => { expect(screen.queryByRole("radiogroup", { name: "Confidential compute type" })).not.toBeInTheDocument(); }); + it("previews the attestation-sidecar resource carve-out when enabled", () => { + setup({ tee: "cpu" }); + + expect(screen.getByText("Requested")).toBeInTheDocument(); + expect(screen.getByText("Attestation sidecar")).toBeInTheDocument(); + expect(screen.getByText("Available to your container")).toBeInTheDocument(); + }); + + it("hides the resource carve-out while confidential compute is off", () => { + setup({}); + + expect(screen.queryByText("Attestation sidecar")).not.toBeInTheDocument(); + }); + + it("warns when cpu-gpu is selected but the service has no GPU resources", () => { + setup({ tee: "cpu-gpu", profile: { hasGpu: false, gpu: 0, gpuModels: [] } }); + + expect(screen.getByText(/needs GPU resources/i)).toBeInTheDocument(); + }); + + it("does not warn when cpu-gpu is selected and GPU was enabled by the selection", async () => { + const { getValues } = setup({ tee: "cpu", profile: { hasGpu: false, gpu: 0, gpuModels: [] } }); + + await userEvent.click(screen.getByRole("radio", { name: "CPU-GPU" })); + + const profile = getValues().services[0].profile; + expect(profile.hasGpu).toBe(true); + expect(profile.gpu).toBeGreaterThanOrEqual(1); + expect((profile.gpuModels ?? []).length).toBeGreaterThanOrEqual(1); + expect(screen.queryByText(/needs GPU resources/i)).not.toBeInTheDocument(); + }); + function setup(input: { tee?: "cpu" | "cpu-gpu"; params?: ServiceType["params"]; profile?: Partial; locked?: boolean }) { const params = input.params ?? (input.tee ? { tee: input.tee } : undefined); const base = defaultServiceWithPlacement({ params }); @@ -135,7 +168,9 @@ describe(ConfidentialComputeCard.name, () => { render( - + + + ); diff --git a/apps/deploy-web/src/components/deployments/ConfigureDeployment/ConfigurationPane/ConfidentialComputeCard/ConfidentialComputeCard.tsx b/apps/deploy-web/src/components/deployments/ConfigureDeployment/ConfigurationPane/ConfidentialComputeCard/ConfidentialComputeCard.tsx index 712d9b09a..b42dc837c 100644 --- a/apps/deploy-web/src/components/deployments/ConfigureDeployment/ConfigurationPane/ConfidentialComputeCard/ConfidentialComputeCard.tsx +++ b/apps/deploy-web/src/components/deployments/ConfigureDeployment/ConfigurationPane/ConfidentialComputeCard/ConfidentialComputeCard.tsx @@ -1,14 +1,16 @@ import type { FC } from "react"; import { useCallback } from "react"; import { useFormContext, useWatch } from "react-hook-form"; -import { CollapsibleCard, Label, RadioGroup, RadioGroupItem } from "@akashnetwork/ui/components"; +import { Alert, CollapsibleCard, Label, RadioGroup, RadioGroupItem } from "@akashnetwork/ui/components"; import { ShieldCheckIcon } from "lucide-react"; +import { ConfidentialComputeResources } from "@src/components/deployments/ConfidentialComputeResources"; import type { SdlBuilderFormValuesType } from "@src/types"; +import { buildFormTeeCarveout } from "@src/utils/confidentialCompute"; import { defaultGpuModel } from "@src/utils/sdl/data"; import { confidentialComputeTooltip } from "../cardTooltips"; -export const DEPENDENCIES = { CollapsibleCard, RadioGroup, RadioGroupItem, Label }; +export const DEPENDENCIES = { CollapsibleCard, RadioGroup, RadioGroupItem, Label, Alert, ConfidentialComputeResources }; type ServiceParams = NonNullable; type TeeType = NonNullable; @@ -50,6 +52,27 @@ export const ConfidentialComputeCard: FC = ({ serviceIndex, locked = fals const tee = useWatch({ control, name: `services.${serviceIndex}.params.tee` }); const isEnabled = tee === "cpu" || tee === "cpu-gpu"; + // Watch the declared resources so the attestation-sidecar preview recomputes as the user edits CPU/RAM. + const serviceProfile = useWatch({ control, name: `services.${serviceIndex}.profile` }); + const count = useWatch({ control, name: `services.${serviceIndex}.count` }); + + // Picking cpu-gpu enables the GPU card, but the user can still turn GPU back off afterwards — surface the + // resulting mismatch since a cpu-gpu enclave attests a GPU and the SDL validator rejects it without one. + const gpuMismatch = tee === "cpu-gpu" && !serviceProfile?.hasGpu; + + const carveout = + isEnabled && tee && serviceProfile + ? buildFormTeeCarveout({ + id: getValues(`services.${serviceIndex}.id`) ?? getValues(`services.${serviceIndex}.title`), + cpu: serviceProfile.cpu, + ram: serviceProfile.ram, + ramUnit: serviceProfile.ramUnit, + count, + gpu: serviceProfile.gpu, + teeType: tee + }) + : undefined; + /** * Brings the GPU card to its enabled state — `profile.hasGpu` on, a count of at least one, and at least * one GPU model — matching what the GPU card's own switch does. Only ever turns GPU on (never off), so a @@ -103,24 +126,39 @@ export const ConfidentialComputeCard: FC = ({ serviceIndex, locked = fals toggleDisabled={locked} > {isEnabled ? ( - setTee(value as TeeType)} className="gap-3" disabled={locked}> - {TEE_OPTIONS.map(option => { - const id = `tee-${serviceIndex}-${option.value}`; - return ( - - - - {option.label} - {option.description} - - - ); - })} - +
+ setTee(value as TeeType)} + className="gap-3" + disabled={locked} + > + {TEE_OPTIONS.map(option => { + const id = `tee-${serviceIndex}-${option.value}`; + return ( + + + + {option.label} + {option.description} + + + ); + })} + + {gpuMismatch && ( + + CPU-GPU confidential compute attests a GPU, so this service needs GPU resources. Enable the GPU card above so providers with confidential-compute + GPUs can bid. + + )} + {carveout && } +
) : (

Confidential compute is off.

)} diff --git a/apps/deploy-web/src/components/deployments/ConfigureDeployment/ConfigurationPane/cardTooltips.tsx b/apps/deploy-web/src/components/deployments/ConfigureDeployment/ConfigurationPane/cardTooltips.tsx index fe995305b..00d86d986 100644 --- a/apps/deploy-web/src/components/deployments/ConfigureDeployment/ConfigurationPane/cardTooltips.tsx +++ b/apps/deploy-web/src/components/deployments/ConfigureDeployment/ConfigurationPane/cardTooltips.tsx @@ -6,6 +6,8 @@ * so stating them here would only risk drifting out of sync. */ +import { CONFIDENTIAL_COMPUTE_DOCS_URL } from "@src/utils/confidentialCompute"; + export const presetsTooltip = ( <> Apply a starting point for this service's compute resources. @@ -76,6 +78,11 @@ export const confidentialComputeTooltip = (

All services in the same placement group must agree on their confidential compute type; conflicting types are rejected when the deployment is validated. +
+
+ + View official documentation + ); diff --git a/apps/deploy-web/src/utils/confidentialCompute.ts b/apps/deploy-web/src/utils/confidentialCompute.ts index 5b7c24395..a145d9ad6 100644 --- a/apps/deploy-web/src/utils/confidentialCompute.ts +++ b/apps/deploy-web/src/utils/confidentialCompute.ts @@ -1,4 +1,5 @@ import type { DeploymentGroup } from "@src/types/deployment"; +import { memoryUnits } from "@src/utils/akash/units"; import { toBase64 } from "@src/utils/encoding"; /** @@ -17,6 +18,12 @@ export type TeeType = "cpu" | "cpu-gpu"; /** On-chain group placement attribute key carrying the declared TEE type. */ export const TEE_TYPE_ATTRIBUTE_KEY = "tee/type"; +/** + * Help link surfaced from the Confidential Compute builder section. Published by akash-network/website + * PR #1245 (docs: confidential compute); live once that PR merges. + */ +export const CONFIDENTIAL_COMPUTE_DOCS_URL = "https://akash.network/docs/learn/core-concepts/confidential-compute"; + /** Exact container name the provider injects (akash-network/provider, webhook/sidecar.go). */ export const ATTESTATION_SIDECAR_SERVICE_NAME = "akash-attestation-sidecar"; @@ -144,6 +151,46 @@ export function getTeeResourceCarveouts(group: DeploymentGroup | undefined | nul return carveouts; } +/** Bytes-per-unit for the builder's memory unit suffixes (Mi/Gi/MB/GB). Unknown suffixes fall back to Mi. */ +const MEMORY_UNIT_BYTES: Record = memoryUnits.reduce>((acc, unit) => { + acc[unit.suffix] = unit.value; + return acc; +}, {}); + +function memoryUnitBytes(unit: string): number { + return MEMORY_UNIT_BYTES[unit] ?? MEGABYTES; +} + +/** + * Builds a single per-pod resource carve-out from the deployment builder's form state, so the builder can + * preview the attestation-sidecar reservation before a lease exists. CPU is declared in whole cores and + * memory as a value + unit suffix, so both are converted to the millicores/bytes that `computeSidecarCarveout` + * expects. Mirrors `getTeeResourceCarveouts` for the on-chain path. + */ +export function buildFormTeeCarveout(input: { + id: string; + cpu: number; + ram: number; + ramUnit: string; + count: number; + gpu?: number; + teeType: TeeType; +}): TeeResourceCarveout { + const cpuMillicores = Math.round(input.cpu * 1000); + const memoryBytes = Math.round(input.ram * memoryUnitBytes(input.ramUnit)); + const { reserved, container } = computeSidecarCarveout({ cpuMillicores, memoryBytes, teeType: input.teeType }); + + return { + id: input.id, + teeType: input.teeType, + count: input.count, + gpuUnits: input.gpu ?? 0, + requested: { cpu: cpuMillicores, memory: memoryBytes }, + reserved, + container + }; +} + interface LeaseStatusLike { services: Record; forwarded_ports?: Record;