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
@@ -0,0 +1,101 @@
import React from "react";
import { describe, expect, it } from "vitest";

import type { TeeResourceCarveout } from "@src/utils/confidentialCompute";
import { ConfidentialComputeResources, DEPENDENCIES } from "./ConfidentialComputeResources";

import { render, screen } from "@testing-library/react";
import { MockComponents } from "@tests/unit/mocks";

const MIB = 1024 * 1024;
const GIB = 1024 * 1024 * 1024;

const cpuCarveout: TeeResourceCarveout = {
id: "1",
teeType: "cpu",
count: 1,
gpuUnits: 0,
requested: { cpu: 500, memory: 256 * MIB },
reserved: { cpu: 100, memory: 64 * MIB },
container: { cpu: 400, memory: 192 * MIB }
};

describe(ConfidentialComputeResources.name, () => {
it("renders nothing when there are no carve-outs", () => {
const { container } = setup({ carveouts: [] });
expect(container).toBeEmptyDOMElement();
});

it("renders the requested, sidecar and available rows with formatted values", () => {
setup({ carveouts: [cpuCarveout] });

expect(screen.getByText("Requested")).toBeInTheDocument();
expect(screen.getByText("Attestation sidecar")).toBeInTheDocument();
expect(screen.getByText("Available to your container")).toBeInTheDocument();

expect(screen.getByText("0.5 CPU")).toBeInTheDocument();
expect(screen.getByText("256 MiB")).toBeInTheDocument();
expect(screen.getByText("0.1 CPU")).toBeInTheDocument();
expect(screen.getByText("64 MiB")).toBeInTheDocument();
expect(screen.getByText("0.4 CPU")).toBeInTheDocument();
expect(screen.getByText("192 MiB")).toBeInTheDocument();
});

it("warns when the declared resources are at or below the attestation sidecar reservation", () => {
const constrained: TeeResourceCarveout = {
...cpuCarveout,
requested: { cpu: 100, memory: 64 * MIB },
reserved: { cpu: 100, memory: 64 * MIB },
container: { cpu: 10, memory: 16 * MIB }
};
setup({ carveouts: [constrained] });
expect(screen.getByText(/minimum/i)).toBeInTheDocument();
});

it("does not warn when the declared resources comfortably exceed the reservation", () => {
setup({ carveouts: [cpuCarveout] });
expect(screen.queryByText(/minimum/i)).not.toBeInTheDocument();
});

it("labels each resource unit by its composition when more than one is present", () => {
const second: TeeResourceCarveout = {
...cpuCarveout,
id: "2",
teeType: "cpu-gpu",
gpuUnits: 1,
requested: { cpu: 8000, memory: 32 * GIB },
reserved: { cpu: 100, memory: 128 * MIB },
container: { cpu: 7900, memory: 32 * GIB - 128 * MIB }
};
setup({ carveouts: [cpuCarveout, second] });

expect(screen.getByText("0.5 CPU · 256 MiB")).toBeInTheDocument();
expect(screen.getByText("8 CPU · 32 GiB · 1 GPU")).toBeInTheDocument();
});

it("notes the replica count when a resource unit runs more than one pod", () => {
setup({ carveouts: [{ ...cpuCarveout, count: 3 }] });
expect(screen.getByText(/3 replicas/i)).toBeInTheDocument();
});

it("does not note replicas for a single-pod resource unit", () => {
setup({ carveouts: [cpuCarveout] });
expect(screen.queryByText(/replicas/i)).not.toBeInTheDocument();
});

it("explains that billing is unaffected via the tooltip", () => {
const CustomTooltip = ({ title, children }: { title: React.ReactNode; children?: React.ReactNode }) => (
<>
{title}
{children}
</>
);
setup({ carveouts: [cpuCarveout], dependencies: { CustomTooltip } });

expect(screen.getByText(/still pay for the full/i)).toBeInTheDocument();
});

function setup(input: { carveouts: TeeResourceCarveout[]; dependencies?: Partial<typeof DEPENDENCIES> }) {
return render(<ConfidentialComputeResources carveouts={input.carveouts} dependencies={MockComponents(DEPENDENCIES, input.dependencies)} />);
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"use client";
import * as React from "react";
import { CustomTooltip } from "@akashnetwork/ui/components";
import { Info } from "lucide-react";

import type { TeeResourceCarveout } from "@src/utils/confidentialCompute";
import { MIN_PRIMARY_CPU_MILLICORES, MIN_PRIMARY_MEMORY_BYTES } from "@src/utils/confidentialCompute";
import { roundDecimal } from "@src/utils/mathHelpers";
import { bytesToShrink } from "@src/utils/unitUtils";

export type Props = {
carveouts: TeeResourceCarveout[];
dependencies?: typeof DEPENDENCIES;
};

export const DEPENDENCIES = { CustomTooltip };

const formatCpu = (millicores: number) => `${roundDecimal(millicores / 1000, 2)} CPU`;

const formatMemory = (bytes: number) => {
const { value, unit } = bytesToShrink(bytes, true);
return `${roundDecimal(value, 2)} ${unit}`;
};

/** Self-describing label for a resource unit (on-chain groups carry no service names). */
const formatUnitLabel = (carveout: TeeResourceCarveout) => {
const parts = [formatCpu(carveout.requested.cpu), formatMemory(carveout.requested.memory)];
if (carveout.gpuUnits > 0) parts.push(`${carveout.gpuUnits} GPU`);
return parts.join(" · ");
};

function ResourceLine({ label, cpu, memory }: { label: string; cpu: number; memory: number }) {
return (
<div className="flex items-center justify-between gap-4 text-sm">
<span className="text-muted-foreground">{label}</span>
<span className="flex items-center gap-2 font-medium">
<span>{formatCpu(cpu)}</span>
<span>{formatMemory(memory)}</span>
</span>
</div>
);
}

export function ConfidentialComputeResources({ carveouts, dependencies: d = DEPENDENCIES }: Props) {
if (carveouts.length === 0) return null;

const multiple = carveouts.length > 1;

return (
<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.">
<Info className="h-3 w-3 cursor-help text-muted-foreground" />
</d.CustomTooltip>
</div>

{carveouts.map(carveout => {
// When the declared budget is at/below the sidecar footprint the provider applies a minimum
// for the primary container, so the rows are no longer a clean subtraction — explain that.
const isConstrained =
carveout.requested.cpu - carveout.reserved.cpu < MIN_PRIMARY_CPU_MILLICORES ||
carveout.requested.memory - carveout.reserved.memory < MIN_PRIMARY_MEMORY_BYTES;

return (
<div key={carveout.id} className="space-y-1 rounded-md border p-3">
{(multiple || carveout.count > 1) && (
<div className="flex items-center justify-between gap-4 text-sm font-medium">
<span>{multiple ? formatUnitLabel(carveout) : null}</span>
{carveout.count > 1 && <span className="text-xs text-muted-foreground">× {carveout.count} replicas</span>}
</div>
)}
<ResourceLine label="Requested" cpu={carveout.requested.cpu} memory={carveout.requested.memory} />
<ResourceLine label="Attestation sidecar" cpu={carveout.reserved.cpu} memory={carveout.reserved.memory} />
<ResourceLine label="Available to your container" cpu={carveout.container.cpu} memory={carveout.container.memory} />
{isConstrained && (
<p className="text-xs text-muted-foreground">
These declared resources are at or below the attestation sidecar&apos;s reservation, so the provider applies a minimum for your container — the
values above are not a direct subtraction. Consider increasing the declared CPU and memory.
</p>
)}
</div>
);
})}
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { DeploymentAlerts } from "@src/components/deployments/DeploymentAlerts/D
import { useServices } from "@src/context/ServicesProvider";
import { useSettings } from "@src/context/SettingsProvider";
import { useWallet } from "@src/context/WalletProvider";
import { useDeclaredTeeTypes } from "@src/hooks/useDeclaredTeeTypes";
import { useFlag } from "@src/hooks/useFlag";
import { useNavigationGuard } from "@src/hooks/useNavigationGuard/useNavigationGuard";
import { useUser } from "@src/hooks/useUser";
Expand Down Expand Up @@ -103,6 +104,7 @@ export const DeploymentDetail: FC<DeploymentDetailProps> = ({ dseq }) => {
}, [deployment, dseq, getLeases, getProviders, address, deploymentLocalStorage]);

const isActive = deployment?.state === "active" && leases?.some(isLeaseLive);
const declaredTeeTypes = useDeclaredTeeTypes(deployment);

const tabs = useMemo(() => {
const tabs: { label: string; value: Tab; badged?: boolean }[] = [
Expand Down Expand Up @@ -242,7 +244,7 @@ export const DeploymentDetail: FC<DeploymentDetailProps> = ({ dseq }) => {
<>
<ReclamationBanner leases={leases} dseq={dseq} />

<DeploymentSubHeader deployment={deployment} leases={leases} />
<DeploymentSubHeader deployment={deployment} leases={leases} teeTypes={declaredTeeTypes} />

<Tabs value={activeTab} onValueChange={value => changeTab(value as Tab)}>
<TabsList
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import formatDistanceToNow from "date-fns/formatDistanceToNow";
import isValid from "date-fns/isValid";
import { WarningCircle } from "iconoir-react";

import { ConfidentialComputeBadge } from "@src/components/shared/ConfidentialComputeBadge";
import { CopyTextToClipboardButton } from "@src/components/shared/CopyTextToClipboardButton";
import { LabelValue } from "@src/components/shared/LabelValue";
import { PricePerTimeUnit } from "@src/components/shared/PricePerTimeUnit";
Expand All @@ -16,16 +17,18 @@ import { useWallet } from "@src/context/WalletProvider";
import { useDeploymentMetrics } from "@src/hooks/useDeploymentMetrics";
import { useTrialDeploymentTimeRemaining } from "@src/hooks/useTrialDeploymentTimeRemaining";
import type { DeploymentDto, LeaseDto } from "@src/types/deployment";
import type { TeeType } from "@src/utils/confidentialCompute";
import { udenomToDenom } from "@src/utils/mathHelpers";
import { isLeaseLive } from "@src/utils/reclamationUtils";

type Props = {
deployment: DeploymentDto;
leases: LeaseDto[] | undefined | null;
teeTypes?: TeeType[];
children?: ReactNode;
};

export const DeploymentSubHeader: React.FunctionComponent<Props> = ({ deployment, leases }) => {
export const DeploymentSubHeader: React.FunctionComponent<Props> = ({ deployment, leases, teeTypes = [] }) => {
const { deploymentCost, realTimeLeft } = useDeploymentMetrics({ deployment, leases });
const isActive = deployment.state === "active";
const hasLeases = !!leases && leases.length > 0;
Expand Down Expand Up @@ -100,6 +103,8 @@ export const DeploymentSubHeader: React.FunctionComponent<Props> = ({ deployment
<div>{deployment.state}</div>
<StatusPill state={deployment.state} size="small" />

<ConfidentialComputeBadge teeTypes={teeTypes} />

{isTrialing && <TrialDeploymentBadge createdHeight={deployment.createdAt} />}
</div>
}
Expand Down
5 changes: 5 additions & 0 deletions apps/deploy-web/src/components/deployments/LeaseRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { SpecDetail } from "@src/components/shared/SpecDetail";
import { StatusPill } from "@src/components/shared/StatusPill";
import { useServices } from "@src/context/ServicesProvider";
import { useProviderCredentials } from "@src/hooks/useProviderCredentials/useProviderCredentials";
import { useTeeResourceCarveouts } from "@src/hooks/useTeeResourceCarveouts";
import { useBidInfo } from "@src/queries/useBidQuery";
import type { LeaseStatusDto } from "@src/queries/useLeaseQuery";
import { useLeaseStatus } from "@src/queries/useLeaseQuery";
Expand All @@ -35,6 +36,7 @@ import { CopyTextToClipboardButton } from "../shared/CopyTextToClipboardButton";
import { ManifestErrorSnackbar } from "../shared/ManifestErrorSnackbar/ManifestErrorSnackbar";
import { ProviderName } from "../shared/ProviderName";
import { ReclamationCard } from "./ReclamationCard/ReclamationCard";
import { ConfidentialComputeResources } from "./ConfidentialComputeResources";

type Props = {
index: number;
Expand Down Expand Up @@ -101,6 +103,7 @@ export const LeaseRow = React.forwardRef<AcceptRefType, Props>(
}, [isLeaseActive, provider, providerCredentials.details, getLeaseStatus, getProviderStatus]);

const parsedManifest = useMemo(() => yaml.load(deploymentManifest), [deploymentManifest]);
const teeCarveouts = useTeeResourceCarveouts(lease);

const checkIfServicesAreAvailable = (leaseStatus: LeaseStatusDto) => {
const servicesNames = leaseStatus ? Object.keys(leaseStatus.services) : [];
Expand Down Expand Up @@ -208,6 +211,8 @@ export const LeaseRow = React.forwardRef<AcceptRefType, Props>(
size="medium"
/>
</div>

<ConfidentialComputeResources carveouts={teeCarveouts} />
<LabelValueOld
label="Price:"
value={
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React from "react";
import { describe, expect, it } from "vitest";

import type { TeeType } from "@src/utils/confidentialCompute";
import { ConfidentialComputeBadge, DEPENDENCIES } from "./ConfidentialComputeBadge";

import { render, screen } from "@testing-library/react";
import { MockComponents } from "@tests/unit/mocks";

const TitleRenderingTooltip = ({ title, children }: { title: React.ReactNode; children?: React.ReactNode }) => (
<>
{title}
{children}
</>
);

describe(ConfidentialComputeBadge.name, () => {
it("renders a friendly CPU label and the TEE explanation for a single cpu TEE type", () => {
setup({ teeTypes: ["cpu"], dependencies: { CustomTooltip: TitleRenderingTooltip } });
expect(screen.getByText("Confidential Compute (CPU)")).toBeInTheDocument();
expect(screen.getByText(/Trusted Execution Environment/)).toBeInTheDocument();
expect(screen.getByText(/depends on the provider honoring the request and passing attestation/i)).toBeInTheDocument();
});

it("renders a friendly CPU + GPU label for a single cpu-gpu TEE type", () => {
setup({ teeTypes: ["cpu-gpu"], dependencies: { CustomTooltip: TitleRenderingTooltip } });
expect(screen.getByText("Confidential Compute (CPU + GPU)")).toBeInTheDocument();
expect(screen.getByText(/Trusted Execution Environment/)).toBeInTheDocument();
});

it("renders a typeless label and lists the distinct types in the tooltip for mixed deployments", () => {
setup({ teeTypes: ["cpu", "cpu-gpu"], dependencies: { CustomTooltip: TitleRenderingTooltip } });

expect(screen.getByText("Confidential Compute")).toBeInTheDocument();
expect(screen.getByText(/CPU, CPU \+ GPU/)).toBeInTheDocument();
});

it("renders nothing when no TEE types are declared", () => {
setup({ teeTypes: [] });
expect(screen.queryByText(/Confidential Compute/)).not.toBeInTheDocument();
});

function setup(input: { teeTypes: TeeType[]; dependencies?: Partial<typeof DEPENDENCIES> }) {
return render(<ConfidentialComputeBadge teeTypes={input.teeTypes} dependencies={MockComponents(DEPENDENCIES, input.dependencies)} />);
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"use client";
import * as React from "react";
import { Badge, CustomTooltip } from "@akashnetwork/ui/components";
import { cn } from "@akashnetwork/ui/utils";
import { Info } from "lucide-react";

import type { TeeType } from "@src/utils/confidentialCompute";
import { formatTeeTypeLabel } from "@src/utils/confidentialCompute";

export type Props = {
teeTypes: TeeType[];
className?: string;
dependencies?: typeof DEPENDENCIES;
};

export const DEPENDENCIES = {
Badge,
CustomTooltip
};

export function ConfidentialComputeBadge({ teeTypes, className, dependencies: d = DEPENDENCIES }: Props) {
if (teeTypes.length === 0) return null;

const label = teeTypes.length === 1 ? `Confidential Compute (${formatTeeTypeLabel(teeTypes[0])})` : "Confidential Compute";

return (
<d.CustomTooltip
title={
<div className="max-w-xs space-y-2 text-sm">
<p>
One or more services in this deployment are configured to run in a Trusted Execution Environment (TEE), keeping their memory encrypted and isolated
from the provider. Confidentiality depends on the provider honoring the request and passing attestation.
</p>
<p>The provider automatically injects an attestation sidecar that reserves a small slice of the resources you declared.</p>
{teeTypes.length > 1 && <p>Declared types: {teeTypes.map(formatTeeTypeLabel).join(", ")}.</p>}
</div>
}
>
<div className="inline-flex items-center gap-1">
<d.Badge variant="secondary" className={cn("inline-flex cursor-help items-center gap-1", className)}>
<span>{label}</span>
<Info className="h-3 w-3" />
</d.Badge>
</div>
</d.CustomTooltip>
);
}
Loading