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
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@ const formatUnitLabel = (carveout: TeeResourceCarveout) => {

function ResourceLine({ label, cpu, memory }: { label: string; cpu: number; memory: number }) {
return (
<div className="flex items-center justify-between gap-4 text-sm">
<div className="flex items-start justify-between gap-3 text-sm">
<span className="text-muted-foreground">{label}</span>
<span className="flex items-center gap-2 font-medium">
{/* Keep each value intact (e.g. "0.01 CPU") and pinned right so a wrapping label can't break the numbers across lines. */}
<span className="flex shrink-0 items-center gap-3 whitespace-nowrap text-right font-medium tabular-nums">
<span>{formatCpu(cpu)}</span>
<span>{formatMemory(memory)}</span>
</span>
Expand All @@ -50,7 +51,10 @@ export function ConfidentialComputeResources({ carveouts, dependencies: d = DEPE
<div className="space-y-3">
<div className="flex items-center gap-1 text-sm font-medium">
<span>Confidential compute resources</span>
<d.CustomTooltip title="The provider injects an attestation sidecar that reserves part of the resources you declared. You still pay for the full declared amount — the reserved slice is taken from what your container can use, not from your bill.">
<d.CustomTooltip
className="max-w-[260px] p-3 text-left font-sans text-xs normal-case text-muted-foreground"
title="The provider injects an attestation sidecar that reserves part of the resources you declared. You still pay for the full declared amount — the reserved slice is taken from what your container can use, not from your bill."
>
<Info className="h-3 w-3 cursor-help text-muted-foreground" />
</d.CustomTooltip>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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();
Comment thread
stalniy marked this conversation as resolved.
});

function setup(input: { tee?: "cpu" | "cpu-gpu"; params?: ServiceType["params"]; profile?: Partial<ServiceType["profile"]>; locked?: boolean }) {
const params = input.params ?? (input.tee ? { tee: input.tee } : undefined);
const base = defaultServiceWithPlacement({ params });
Expand All @@ -135,7 +168,9 @@ describe(ConfidentialComputeCard.name, () => {

render(
<Wrapper>
<ConfidentialComputeCard serviceIndex={0} locked={input.locked} dependencies={{ ...DEPENDENCIES }} />
<TooltipProvider>
<ConfidentialComputeCard serviceIndex={0} locked={input.locked} dependencies={{ ...DEPENDENCIES }} />
</TooltipProvider>
</Wrapper>
);

Expand Down
Original file line number Diff line number Diff line change
@@ -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<SdlBuilderFormValuesType["services"][number]["params"]>;
type TeeType = NonNullable<ServiceParams["tee"]>;
Expand Down Expand Up @@ -50,6 +52,27 @@ export const ConfidentialComputeCard: FC<Props> = ({ 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
Expand Down Expand Up @@ -103,24 +126,39 @@ export const ConfidentialComputeCard: FC<Props> = ({ serviceIndex, locked = fals
toggleDisabled={locked}
>
{isEnabled ? (
<d.RadioGroup aria-label="Confidential compute type" value={tee} onValueChange={value => setTee(value as TeeType)} className="gap-3" disabled={locked}>
{TEE_OPTIONS.map(option => {
const id = `tee-${serviceIndex}-${option.value}`;
return (
<d.Label
key={option.value}
htmlFor={id}
className="flex items-start gap-3 rounded-md border border-zinc-200 p-3 font-normal dark:border-zinc-800"
>
<d.RadioGroupItem id={id} value={option.value} aria-label={option.label} disabled={locked} className="mt-0.5" />
<span className="flex flex-col gap-0.5">
<span className="text-sm font-medium">{option.label}</span>
<span className="text-xs text-muted-foreground">{option.description}</span>
</span>
</d.Label>
);
})}
</d.RadioGroup>
<div className="space-y-4">
<d.RadioGroup
aria-label="Confidential compute type"
value={tee}
onValueChange={value => setTee(value as TeeType)}
className="gap-3"
disabled={locked}
>
{TEE_OPTIONS.map(option => {
const id = `tee-${serviceIndex}-${option.value}`;
return (
<d.Label
key={option.value}
htmlFor={id}
className="flex items-start gap-3 rounded-md border border-zinc-200 p-3 font-normal dark:border-zinc-800"
>
<d.RadioGroupItem id={id} value={option.value} aria-label={option.label} disabled={locked} className="mt-0.5" />
<span className="flex flex-col gap-0.5">
<span className="text-sm font-medium">{option.label}</span>
<span className="text-xs text-muted-foreground">{option.description}</span>
</span>
</d.Label>
);
})}
</d.RadioGroup>
{gpuMismatch && (
<d.Alert variant="warning">
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.
</d.Alert>
)}
{carveout && <d.ConfidentialComputeResources carveouts={[carveout]} />}
</div>
) : (
<p className="text-sm text-muted-foreground">Confidential compute is off.</p>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -76,6 +78,11 @@ export const confidentialComputeTooltip = (
<br />
<br />
All services in the same placement group must agree on their confidential compute type; conflicting types are rejected when the deployment is validated.
<br />
<br />
<a href={CONFIDENTIAL_COMPUTE_DOCS_URL} target="_blank" rel="noopener">
View official documentation
</a>
</>
);

Expand Down
47 changes: 47 additions & 0 deletions apps/deploy-web/src/utils/confidentialCompute.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { DeploymentGroup } from "@src/types/deployment";
import { memoryUnits } from "@src/utils/akash/units";
import { toBase64 } from "@src/utils/encoding";

/**
Expand All @@ -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";

Expand Down Expand Up @@ -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<string, number> = memoryUnits.reduce<Record<string, number>>((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<string, unknown>;
forwarded_ports?: Record<string, unknown>;
Expand Down