Skip to content

Commit

Permalink
refactor: save certificate on create, remove `set_mutual_tls_certific…
Browse files Browse the repository at this point in the history
…ate` method,
  • Loading branch information
ilbertt committed Jun 6, 2024
1 parent 1c7490a commit 9c94891
Show file tree
Hide file tree
Showing 13 changed files with 194 additions and 229 deletions.
101 changes: 44 additions & 57 deletions frontend/app/dashboard/new-deployment/page.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,33 @@
"use client";

import {BackButton} from "@/components/back-button";
import {useToast} from "@/components/ui/use-toast";
import {useDeploymentContext} from "@/contexts/DeploymentContext";
import { BackButton } from "@/components/back-button";
import { useToast } from "@/components/ui/use-toast";
import { useDeploymentContext } from "@/contexts/DeploymentContext";
import {
type OnWsErrorCallback,
type OnWsMessageCallback,
type OnWsOpenCallback,
useIcContext,
} from "@/contexts/IcContext";
import type {DeploymentParams, DeploymentState} from "@/declarations/backend.did";
import {extractDeploymentCreated} from "@/helpers/deployment";
import {extractOk} from "@/helpers/result";
import {sendManifestToProvider} from "@/services/deployment";
import {useRouter} from "next/navigation";
import {useCallback, useEffect, useMemo, useState} from "react";
import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert";
import {AlertCircle} from "lucide-react";
import {displayE8sAsIcp, icpToE8s} from "@/helpers/ui";
import {Spinner} from "@/components/spinner";
import {NewDeploymentForm} from "@/components/new-deployment-form";
import {transferE8sToBackend} from "@/services/backend";
import type { DeploymentParams, DeploymentState } from "@/declarations/backend.did";
import { extractDeploymentCreated } from "@/helpers/deployment";
import { extractOk } from "@/helpers/result";
import { sendManifestToProvider } from "@/services/deployment";
import { useRouter } from "next/navigation";
import { useCallback, useEffect, useMemo, useState } from "react";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { AlertCircle } from "lucide-react";
import { displayE8sAsIcp, icpToE8s } from "@/helpers/ui";
import { Spinner } from "@/components/spinner";
import { NewDeploymentForm } from "@/components/new-deployment-form";
import { transferE8sToBackend } from "@/services/backend";

const FETCH_DEPLOYMENT_PRICE_INTERVAL_MS = 30_000; // 30 seconds

export default function NewDeployment() {
const router = useRouter();
const {backendActor, openWs, closeWs, setWsCallbacks, ledgerCanister, ledgerData, refreshLedgerData} = useIcContext();
const {tlsCertificateData, loadOrCreateCertificate, fetchDeployments} =
const { backendActor, openWs, closeWs, setWsCallbacks, ledgerCanister, ledgerData, refreshLedgerData } = useIcContext();
const { tlsCertificateData, loadOrCreateCertificate, fetchDeployments } =
useDeploymentContext();
const [isSubmitting, setIsSubmitting] = useState(false);
const [isDeploying, setIsDeploying] = useState(false);
Expand All @@ -38,13 +38,12 @@ export default function NewDeployment() {
const [deploymentE8sPrice, setDeploymentE8sPrice] = useState<bigint | null>(null);
const [fetchDeploymentPriceInterval, setFetchDeploymentPriceInterval] = useState<NodeJS.Timeout | null>(null);
const userHasEnoughBalance = useMemo(() =>
ledgerData.balanceE8s !== null && deploymentE8sPrice !== null && ledgerData.balanceE8s > deploymentE8sPrice,
ledgerData.balanceE8s !== null && deploymentE8sPrice !== null && ledgerData.balanceE8s > deploymentE8sPrice,
[ledgerData.balanceE8s, deploymentE8sPrice]
);
const [certificateStatus, setCertificateStatus] = useState<string | null>(null);
const [paymentStatus, setPaymentStatus] = useState<string | null>(null);
const [deploymentParams, setDeploymentParams] = useState<DeploymentParams | null>(null);
const {toast} = useToast();
const { toast } = useToast();

const toastError = useCallback(
(message: string) => {
Expand All @@ -59,7 +58,7 @@ export default function NewDeployment() {
[toast]
);

const sendIcpToBackend = async (userCanisterBalance: number) => {
const sendIcpToBackend = useCallback(async (userCanisterBalance: number) => {
if (!backendActor || !ledgerCanister) {
toastError("Backend actor or ledger canister not found");
return;
Expand Down Expand Up @@ -89,8 +88,6 @@ export default function NewDeployment() {
await refreshLedgerData();

setPaymentStatus(prev => prev + " DONE");

await handleDeploy(deploymentParams!);
} catch (e) {
console.error("Failed to transfer funds:", e);
toastError("Failed to transfer funds, see console for details");
Expand All @@ -99,14 +96,12 @@ export default function NewDeployment() {
setIsSubmitting(false);
return;
}
}
}, [backendActor, deploymentE8sPrice, ledgerCanister, refreshLedgerData, toastError]);

const onWsOpen: OnWsOpenCallback = useCallback(async () => {
console.log("ws open");

setIsDeploying(true);
setIsSubmitting(false);
try {
const createDeployment = async () => {
if (!backendActor) {
throw new Error("No backend actor");
}
Expand All @@ -117,25 +112,31 @@ export default function NewDeployment() {

const res = await backendActor.create_deployment(deploymentParams);
const deploymentId = extractOk(res);

console.log("deployment id", deploymentId);
setDeploymentSteps([{ Initialized: null }]);
};

setDeploymentSteps([{Initialized: null}]);
setIsDeploying(true);
setIsSubmitting(false);
try {
await createDeployment();
} catch (e: any) {
console.error("Failed to create deployment:", e);

setIsDeploying(false);

if (e.message.startsWith("Not enough ICP balance")) {
setDeploymentError("Failed to create deployment, insufficient balance. Auto top-up initiated.");
if (e.message.startsWith("Not enough balance. Required: ")) {
console.warn("Failed to create deployment, insufficient balance. Auto top-up initiated.");

const userCanisterBalance = parseInt(e.message.split(" ")[6]);
const userCanisterBalance = parseInt(e.message.replace("Not enough balance. Required: ", "").replace(" ICP", ""));
await sendIcpToBackend(userCanisterBalance);

await createDeployment();
} else {
setDeploymentError("Failed to create deployment, see console for details");
}
}
}, [backendActor, deploymentParams]);
}, [backendActor, deploymentParams, sendIcpToBackend]);

const onWsMessage: OnWsMessageCallback = useCallback(
async (ev) => {
Expand All @@ -158,12 +159,12 @@ export default function NewDeployment() {

try {
if ("LeaseCreated" in deploymentUpdate.update) {
const {manifest_sorted_json, dseq} = extractDeploymentCreated(
const { manifest_sorted_json, dseq } = extractDeploymentCreated(
deploymentSteps.find((el) =>
el.hasOwnProperty("DeploymentCreated")
)!
);
const {provider_url} = deploymentUpdate.update.LeaseCreated;
const { provider_url } = deploymentUpdate.update.LeaseCreated;

const manifestUrl = new URL(
`/deployment/${dseq}/manifest`,
Expand Down Expand Up @@ -267,15 +268,14 @@ export default function NewDeployment() {
setDeploymentParams(values);
setIsDeploying(false);
setIsSubmitting(true);
setCertificateStatus(null);

try {
setCertificateStatus("Retrieving certificate...");
console.log("Retrieving mTLS certificate...");
const cert = await loadOrCreateCertificate(backendActor!);
if (!cert) {
throw new Error("No certificate");
}
setCertificateStatus("Certificate retrieved");
console.log("mTLS certificate retrieved");

if (fetchDeploymentPriceInterval !== null) {
clearInterval(fetchDeploymentPriceInterval);
Expand Down Expand Up @@ -304,9 +304,6 @@ export default function NewDeployment() {
userHasEnoughBalance,
toastError,
backendActor,
ledgerCanister,
refreshLedgerData,
deploymentE8sPrice,
fetchDeploymentPriceInterval,
loadOrCreateCertificate,
]);
Expand All @@ -321,10 +318,6 @@ export default function NewDeployment() {
const icpPrice = extractOk(res);

// add 1% ICP to cover price fluctuation
//
// TODO: try to deploy and parse the error from the canister.
// If the error is a not enough balance error, then we can
// send funds to the canister and try again.
setDeploymentE8sPrice(icpToE8s(icpPrice * 1.01));
} catch (e) {
console.error("Failed to fetch deployment price:", e);
Expand Down Expand Up @@ -359,7 +352,7 @@ export default function NewDeployment() {
return (
<div className="flex-1 space-y-4 p-8 pt-6">
<div className="flex items-center justify-start">
<BackButton/>
<BackButton />
<h2 className="ml-4 text-3xl font-bold tracking-tight">
Create Deployment
</h2>
Expand All @@ -378,13 +371,13 @@ export default function NewDeployment() {
Price (est.):
</h5>
{deploymentE8sPrice !== null ? (
<pre>~{displayE8sAsIcp(deploymentE8sPrice, {maximumFractionDigits: 6})}</pre>
<pre>~{displayE8sAsIcp(deploymentE8sPrice, { maximumFractionDigits: 6 })}</pre>
) : (
<Spinner/>
<Spinner />
)}
{(!(isSubmitting || isDeploying) && (deploymentE8sPrice !== null) && !userHasEnoughBalance) && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4"/>
<AlertCircle className="h-4 w-4" />
<AlertTitle>Insufficient balance</AlertTitle>
<AlertDescription>
<p>Please top up your account.</p>
Expand All @@ -400,12 +393,6 @@ export default function NewDeployment() {
</Alert>
)}
</div>
{certificateStatus && (
<div className="flex flex-col gap-3">
<h5 className="font-bold">Certificate Status:</h5>
<p>{certificateStatus}</p>
</div>
)}
{paymentStatus && (
<div className="flex flex-col gap-3">
<h5 className="font-bold">Payment Status:</h5>
Expand All @@ -423,13 +410,13 @@ export default function NewDeployment() {
{idx + 1}. {el}
</p>
))}
{isDeploying && <Spinner/>}
{isDeploying && <Spinner />}
</div>
</div>
)}
{Boolean(deploymentError) && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4"/>
<AlertCircle className="h-4 w-4" />
<AlertTitle>Deployment Error</AlertTitle>
<AlertDescription>
<p>{deploymentError}</p>
Expand Down
31 changes: 11 additions & 20 deletions frontend/contexts/DeploymentContext.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {type GetDeploymentsResult} from "@/declarations/backend.did";
import {getDeploymentCreatedDate} from "@/helpers/deployment";
import {extractOk, type OkType} from "@/helpers/result";
import {createX509, X509CertificateData} from "@/lib/certificate";
import {type BackendActor} from "@/services/backend";
import {createContext, useCallback, useContext, useState} from "react";
import {getCurrentUser, setUserMutualTlsCertificate} from "@/services/user";
import { type GetDeploymentsResult } from "@/declarations/backend.did";
import { getDeploymentCreatedDate } from "@/helpers/deployment";
import { extractOk, type OkType } from "@/helpers/result";
import { createX509, X509CertificateData } from "@/lib/certificate";
import { type BackendActor } from "@/services/backend";
import { createContext, useCallback, useContext, useState } from "react";
import { getCurrentUser } from "@/services/user";

export type Deployments = OkType<GetDeploymentsResult>;

Expand All @@ -21,7 +21,7 @@ type DeploymentProviderProps = {
children?: React.ReactNode;
}

export const DeploymentProvider: React.FC<DeploymentProviderProps> = ({children}) => {
export const DeploymentProvider: React.FC<DeploymentProviderProps> = ({ children }) => {
const [tlsCertificateData, setCertificateData] = useState<X509CertificateData | null>(null);
const [deployments, setDeployments] = useState<Deployments>([]);

Expand All @@ -39,22 +39,13 @@ export const DeploymentProvider: React.FC<DeploymentProviderProps> = ({children}
}, []);

const loadOrCreateCertificate = useCallback(async (actor: BackendActor): Promise<X509CertificateData | null> => {
const mutualTlsCertificateStringified = (await getCurrentUser(actor)).mutual_tls_certificate;
let certData = null;
if (mutualTlsCertificateStringified) {
certData = JSON.parse(mutualTlsCertificateStringified);
}

let certData = (await getCurrentUser(actor)).mtls_certificate[0] || null;

if (!certData) {
try {
const akashAddress = extractOk(await actor.address());
certData = await createX509(akashAddress);
extractOk(await actor.create_certificate(
Buffer.from(certData.cert, "utf-8").toString("base64"),
Buffer.from(certData.pubKey, "utf-8").toString("base64"),
));

await setUserMutualTlsCertificate(actor, JSON.stringify(certData));
extractOk(await actor.create_certificate(certData));
} catch (e) {
console.error(e);
alert("Failed to create certificate, see console for details");
Expand Down
10 changes: 7 additions & 3 deletions frontend/declarations/backend.did.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ export interface LogsFilterRequest {
'after_timestamp_ms' : [] | [bigint],
'before_timestamp_ms' : [] | [bigint],
}
export interface MTlsCertificateData {
'cert' : string,
'pub_key' : string,
'priv_key' : string,
}
export type Memo = bigint;
export type MemorySize = { 'Large' : null } |
{ 'Small' : null } |
Expand Down Expand Up @@ -231,9 +236,9 @@ export type TransferFeeArg = {};
export type TransferResult = { 'Ok' : BlockIndex } |
{ 'Err' : TransferError };
export interface User {
'mutual_tls_certificate' : string,
'akt_balance' : number,
'payments' : BigUint64Array | bigint[],
'mtls_certificate' : [] | [MTlsCertificateData],
'role' : UserRole,
'created_at' : TimestampNs,
}
Expand All @@ -252,7 +257,7 @@ export interface _SERVICE {
'balance' : ActorMethod<[], ApiNatResult>,
'check_tx' : ActorMethod<[string], ApiEmptyResult>,
'close_deployment' : ActorMethod<[string], ApiEmptyResult>,
'create_certificate' : ActorMethod<[string, string], ApiStringResult>,
'create_certificate' : ActorMethod<[MTlsCertificateData], ApiStringResult>,
'create_deployment' : ActorMethod<[DeploymentParams], CreateDeploymentResult>,
'create_test_deployment' : ActorMethod<[], CreateDeploymentResult>,
'create_user' : ActorMethod<[], CreateUserResult>,
Expand All @@ -265,7 +270,6 @@ export interface _SERVICE {
'list_logs' : ActorMethod<[LogsFilterRequest], ListLogsResponse>,
'promote_user_to_admin' : ActorMethod<[UserId], ApiEmptyResult>,
'query_blocks' : ActorMethod<[GetBlocksArgs], QueryBlocksResult>,
'set_mutual_tls_certificate' : ActorMethod<[string], ApiEmptyResult>,
'update_akt_balance' : ActorMethod<[bigint], ApiFloatResult>,
'update_deployment_state' : ActorMethod<
[string, DeploymentState],
Expand Down
10 changes: 7 additions & 3 deletions frontend/declarations/backend.did.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ export const idlFactory = ({ IDL }) => {
const ApiStringResult = IDL.Variant({ 'Ok' : IDL.Text, 'Err' : ApiError });
const ApiNatResult = IDL.Variant({ 'Ok' : IDL.Nat64, 'Err' : ApiError });
const ApiEmptyResult = IDL.Variant({ 'Ok' : IDL.Null, 'Err' : ApiError });
const MTlsCertificateData = IDL.Record({
'cert' : IDL.Text,
'pub_key' : IDL.Text,
'priv_key' : IDL.Text,
});
const CpuSize = IDL.Variant({
'Large' : IDL.Null,
'Small' : IDL.Null,
Expand Down Expand Up @@ -78,9 +83,9 @@ export const idlFactory = ({ IDL }) => {
});
const UserRole = IDL.Variant({ 'Admin' : IDL.Null, 'Deployer' : IDL.Null });
const User = IDL.Record({
'mutual_tls_certificate' : IDL.Text,
'akt_balance' : IDL.Float64,
'payments' : IDL.Vec(IDL.Nat64),
'mtls_certificate' : IDL.Opt(MTlsCertificateData),
'role' : UserRole,
'created_at' : TimestampNs,
});
Expand Down Expand Up @@ -249,7 +254,7 @@ export const idlFactory = ({ IDL }) => {
'check_tx' : IDL.Func([IDL.Text], [ApiEmptyResult], []),
'close_deployment' : IDL.Func([IDL.Text], [ApiEmptyResult], []),
'create_certificate' : IDL.Func(
[IDL.Text, IDL.Text],
[MTlsCertificateData],
[ApiStringResult],
[],
),
Expand Down Expand Up @@ -277,7 +282,6 @@ export const idlFactory = ({ IDL }) => {
[QueryBlocksResult],
['composite_query'],
),
'set_mutual_tls_certificate' : IDL.Func([IDL.Text], [ApiEmptyResult], []),
'update_akt_balance' : IDL.Func([IDL.Nat64], [ApiFloatResult], []),
'update_deployment_state' : IDL.Func(
[IDL.Text, DeploymentState],
Expand Down
Loading

0 comments on commit 9c94891

Please sign in to comment.