Skip to content

Commit

Permalink
feat: enclave manager indexer client (#1643)
Browse files Browse the repository at this point in the history
## Description:
This PR adds a typed indexer client into the new enclave manager code.
This client will be used to generate the forms for configuring enclaves
and eventually for listing the catalog.

Additionally this PR:
* refactors the current kurtosis client into `client/enclaveManager`.
* uses the `check` endpoint in `KurtosisClient` to perform a basic
connectivity check when `KurtosisClientContext` starts up.

I chose not to extend the `KurtosisClient` from the
`KurtosisPackageIndexerClient` as whilst that would have been
convenient, they have different responsibilities.

## Is this change user facing?
NO (the new enclave manager is not live)

## References (if applicable):
This PR is based on #1639
  • Loading branch information
Dartoxian authored Oct 30, 2023
1 parent 5fe2bb6 commit b0af9d8
Show file tree
Hide file tree
Showing 54 changed files with 2,573 additions and 247 deletions.
2 changes: 2 additions & 0 deletions enclave-manager/web/.env
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
REACT_APP_KURTOSIS_DEFAULT_HOST=localhost
REACT_APP_KURTOSIS_DEFAULT_PORT=8081

REACT_APP_KURTOSIS_CLOUD_URL=https://cloud.kurtosis.com:9770
3 changes: 2 additions & 1 deletion enclave-manager/web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
This codebase produces the enclave manager UI (ie `kurtosis web`). The `src` directory contains:

- `components` - components used in the application. This includes theme definitions and application context definitions
- `client` - libraries for interacting with the local `kurtosis` backend - used to instantiate a `KurtosisAppContext` and interacted with using `useKurtosis`
- `client/enclaveManager` - libraries for interacting with the local `kurtosis` backend - used to instantiate a `KurtosisClientContext` and interacted with using `useKurtosisClient`
- `client/packageIndexer` - libraries for interacting with the package indexer - used to instantiate a `KurtosisPackageIndexerClientContext` and interacted with using `useKurtosisPackageIndexerClient`
- `emui` - the composition of the above to produce the Enclave Manager UI using react router

## Available Scripts
Expand Down
2 changes: 2 additions & 0 deletions enclave-manager/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
"luxon": "^3.4.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.47.0",
"react-icons": "^4.11.0",
"react-markdown": "^9.0.0",
"react-router-dom": "^6.17.0",
"react-scripts": "5.0.1",
"true-myth": "^7.1.0"
Expand Down
2 changes: 2 additions & 0 deletions enclave-manager/web/src/client/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ export const KURTOSIS_DEFAULT_PORT = isDefined(process.env.REACT_APP_KURTOSIS_DE
: 8081;
export const KURTOSIS_DEFAULT_URL =
process.env.REACT_APP_KURTOSIS_DEFAULT_URL || `http://${KURTOSIS_DEFAULT_HOST}:${KURTOSIS_DEFAULT_PORT}`;

export const KURTOSIS_CLOUD_URL = process.env.REACT_APP_KURTOSIS_CLOUD_URL || "https://cloud.kurtosis.com:9770";
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createPromiseClient } from "@connectrpc/connect";
import { createConnectTransport } from "@connectrpc/connect-web";
import { KurtosisEnclaveManagerServer } from "enclave-manager-sdk/build/kurtosis_enclave_manager_api_connect";
import { KURTOSIS_DEFAULT_PORT } from "./constants";
import { KURTOSIS_DEFAULT_PORT } from "../constants";
import { KurtosisClient } from "./KurtosisClient";

function constructGatewayURL(host: string): string {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import { PromiseClient } from "@connectrpc/connect";
import { EnclaveInfo } from "enclave-manager-sdk/build/engine_service_pb";
import { RunStarlarkPackageArgs } from "enclave-manager-sdk/build/api_container_service_pb";
import {
CreateEnclaveArgs,
DestroyEnclaveArgs,
EnclaveInfo,
EnclaveMode,
} from "enclave-manager-sdk/build/engine_service_pb";
import { KurtosisEnclaveManagerServer } from "enclave-manager-sdk/build/kurtosis_enclave_manager_api_connect";
import {
GetListFilesArtifactNamesAndUuidsRequest,
GetServicesRequest,
GetStarlarkRunRequest,
RunStarlarkPackageRequest,
} from "enclave-manager-sdk/build/kurtosis_enclave_manager_api_pb";
import { assertDefined, asyncResult } from "../utils";
import { assertDefined, asyncResult } from "../../utils";
import { RemoveFunctions } from "../../utils/types";

export abstract class KurtosisClient {
protected client: PromiseClient<typeof KurtosisEnclaveManagerServer>;
Expand All @@ -17,11 +25,22 @@ export abstract class KurtosisClient {

abstract getHeaderOptions(): { headers?: Headers };

async checkHealth() {
return asyncResult(this.client.check({}, this.getHeaderOptions()));
}

async getEnclaves() {
return asyncResult(this.client.getEnclaves({}, this.getHeaderOptions()), "KurtosisClient could not getEnclaves");
}

async getServices(enclave: EnclaveInfo) {
async destroy(enclaveUUID: string) {
return asyncResult(
this.client.destroyEnclave(new DestroyEnclaveArgs({ enclaveIdentifier: enclaveUUID }), this.getHeaderOptions()),
`KurtosisClient could not destroy enclave ${enclaveUUID}`,
);
}

async getServices(enclave: RemoveFunctions<EnclaveInfo>) {
return await asyncResult(() => {
const apicInfo = enclave.apiContainerInfo;
assertDefined(apicInfo, `Cannot getServices because the passed enclave '${enclave.name}' does not have apicInfo`);
Expand All @@ -33,7 +52,7 @@ export abstract class KurtosisClient {
}, "KurtosisClient could not getServices");
}

async getStarlarkRun(enclave: EnclaveInfo) {
async getStarlarkRun(enclave: RemoveFunctions<EnclaveInfo>) {
return await asyncResult(() => {
const apicInfo = enclave.apiContainerInfo;
assertDefined(
Expand All @@ -48,7 +67,7 @@ export abstract class KurtosisClient {
}, "KurtosisClient could not getStarlarkRun");
}

async listFilesArtifactNamesAndUuids(enclave: EnclaveInfo) {
async listFilesArtifactNamesAndUuids(enclave: RemoveFunctions<EnclaveInfo>) {
return await asyncResult(() => {
const apicInfo = enclave.apiContainerInfo;
assertDefined(
Expand All @@ -62,4 +81,40 @@ export abstract class KurtosisClient {
return this.client.listFilesArtifactNamesAndUuids(request, this.getHeaderOptions());
}, "KurtosisClient could not listFilesArtifactNamesAndUuids");
}

async createEnclave(
enclaveName: string,
apiContainerLogLevel: string,
productionMode?: boolean,
apiContainerVersionTag?: string,
) {
return asyncResult(() => {
const request = new CreateEnclaveArgs({
enclaveName,
apiContainerLogLevel,
mode: productionMode ? EnclaveMode.PRODUCTION : EnclaveMode.TEST,
apiContainerVersionTag: apiContainerVersionTag || "",
});
return this.client.createEnclave(request, this.getHeaderOptions());
});
}

async runStarlarkPackage(enclave: RemoveFunctions<EnclaveInfo>, packageId: string, args: Record<string, any>) {
// Not currently using asyncResult as the return type here is an asyncIterable
const apicInfo = enclave.apiContainerInfo;
assertDefined(
apicInfo,
`Cannot listFilesArtifactNamesAndUuids because the passed enclave '${enclave.name}' does not have apicInfo`,
);
const request = new RunStarlarkPackageRequest({
apicIpAddress: apicInfo.bridgeIpAddress,
apicPort: apicInfo.grpcPortInsideEnclave,
RunStarlarkPackageArgs: new RunStarlarkPackageArgs({
dryRun: false,
packageId: packageId,
serializedParams: JSON.stringify(args),
}),
});
return this.client.runStarlarkPackage(request, this.getHeaderOptions());
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Alert, AlertDescription, AlertIcon, AlertTitle, Flex, Heading, Spinner, useToast } from "@chakra-ui/react";
import { Flex, Heading, Spinner, useToast } from "@chakra-ui/react";
import { createContext, PropsWithChildren, useContext, useEffect, useMemo, useState } from "react";
import { assertDefined, isDefined, isStringTrue, stringifyError } from "../utils";
import { KurtosisAlert } from "../../components/KurtosisAlert";
import { assertDefined, isDefined, isStringTrue, stringifyError } from "../../utils";
import { AuthenticatedKurtosisClient } from "./AuthenticatedKurtosisClient";
import { KurtosisClient } from "./KurtosisClient";
import { LocalKurtosisClient } from "./LocalKurtosisClient";
Expand Down Expand Up @@ -36,7 +37,6 @@ export const KurtosisClientProvider = ({ children }: PropsWithChildren) => {
title: "Error",
description: r.error.message,
status: "error",
position: "top",
variant: "solid",
});
}
Expand Down Expand Up @@ -70,25 +70,36 @@ export const KurtosisClientProvider = ({ children }: PropsWithChildren) => {
}, []);

useEffect(() => {
const searchParams = new URLSearchParams(window.location.search);
const requireAuth = isStringTrue(searchParams.get("require_authentication"));
const requestedApiHost = searchParams.get("api_host");
// eslint-disable-next-line
const preloadedPackage = searchParams.get("package");
try {
setError(undefined);
if (requireAuth) {
assertDefined(requestedApiHost, `The parameter 'requestedApiHost' is not defined`);
if (isDefined(jwtToken)) {
setClient(new AuthenticatedKurtosisClient(requestedApiHost, jwtToken));
(async () => {
const searchParams = new URLSearchParams(window.location.search);
const requireAuth = isStringTrue(searchParams.get("require_authentication"));
const requestedApiHost = searchParams.get("api_host");
// eslint-disable-next-line
const preloadedPackage = searchParams.get("package");
try {
setError(undefined);
let newClient: KurtosisClient | null = null;
if (requireAuth) {
assertDefined(requestedApiHost, `The parameter 'requestedApiHost' is not defined`);
if (isDefined(jwtToken)) {
newClient = new AuthenticatedKurtosisClient(requestedApiHost, jwtToken);
}
} else {
newClient = new LocalKurtosisClient();
}
if (isDefined(newClient)) {
const checkResp = await newClient.checkHealth();
if (checkResp.isErr) {
setError("Cannot reach the enclave manager backend - is your enclave manager definitely running?");
return;
}
setClient(newClient);
}
} else {
setClient(new LocalKurtosisClient());
} catch (e: any) {
console.error(e);
setError(stringifyError(e));
}
} catch (e: any) {
console.error(e);
setError(stringifyError(e));
}
})();
}, [jwtToken]);

if (errorHandlingClient) {
Expand All @@ -108,13 +119,7 @@ export const KurtosisClientProvider = ({ children }: PropsWithChildren) => {
</Heading>
</>
)}
{isDefined(error) && (
<Alert status="error">
<AlertIcon />
<AlertTitle>Error:</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{isDefined(error) && <KurtosisAlert message={error} />}
</Flex>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createPromiseClient } from "@connectrpc/connect";
import { createConnectTransport } from "@connectrpc/connect-web";
import { KurtosisEnclaveManagerServer } from "enclave-manager-sdk/build/kurtosis_enclave_manager_api_connect";
import { KURTOSIS_DEFAULT_URL } from "./constants";
import { KURTOSIS_DEFAULT_URL } from "../constants";
import { KurtosisClient } from "./KurtosisClient";

export class LocalKurtosisClient extends KurtosisClient {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { createPromiseClient, PromiseClient } from "@connectrpc/connect";
import { createConnectTransport } from "@connectrpc/connect-web";
import { asyncResult } from "../../utils";
import { KURTOSIS_CLOUD_URL } from "../constants";
import { KurtosisPackageIndexer } from "./api/kurtosis_package_indexer_connect";
import { ReadPackageRequest } from "./api/kurtosis_package_indexer_pb";

export class KurtosisPackageIndexerClient {
private client: PromiseClient<typeof KurtosisPackageIndexer>;

constructor() {
this.client = createPromiseClient(KurtosisPackageIndexer, createConnectTransport({ baseUrl: KURTOSIS_CLOUD_URL }));
}

getPackages = async () => {
return asyncResult(() => {
return this.client.getPackages({});
});
};

readPackage = async (packageUrl: string) => {
return asyncResult(() => {
const components = packageUrl.split("/");
if (components.length < 3) {
throw Error(`Illegal url, invalid number of components: ${packageUrl}`);
}
if (components[1].length < 1 || components[2].length < 1) {
throw Error(`Illegal url, empty components: ${packageUrl}`);
}
return this.client.readPackage(
new ReadPackageRequest({
repositoryMetadata: {
baseUrl: "github.com",
owner: components[1],
name: components[2],
},
}),
);
});
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { useToast } from "@chakra-ui/react";
import { createContext, PropsWithChildren, useContext, useMemo } from "react";
import { assertDefined } from "../../utils";
import { KurtosisPackageIndexerClient } from "./KurtosisPackageIndexerClient";

type KurtosisPackageIndexerClientContextState = {
client: KurtosisPackageIndexerClient | null;
};

const KurtosisPackageIndexerClientContext = createContext<KurtosisPackageIndexerClientContextState>({ client: null });

export const KurtosisPackageIndexerProvider = ({ children }: PropsWithChildren) => {
const toast = useToast();

const errorHandlingClient = useMemo(() => {
return new Proxy(new KurtosisPackageIndexerClient(), {
get(target, prop: string | symbol) {
if (prop === "getPackages" || prop === "readPackage") {
return new Proxy(target[prop], {
apply: (target, thisArg, argumentsList) => {
const methodResult = Reflect.apply(target, thisArg, argumentsList) as ReturnType<typeof target>;
return methodResult.then((r) => {
if (r.isErr) {
toast({
title: "Error",
description: r.error.message,
status: "error",
variant: "solid",
});
}
return r;
});
},
});
} else {
return Reflect.get(target, prop);
}
},
});
}, [toast]);

return (
<KurtosisPackageIndexerClientContext.Provider value={{ client: errorHandlingClient }}>
{children}
</KurtosisPackageIndexerClientContext.Provider>
);
};

export const useKurtosisPackageIndexerClient = (): KurtosisPackageIndexerClient => {
const { client } = useContext(KurtosisPackageIndexerClientContext);

assertDefined(
client,
`useKurtosisPackageIndexerClient used incorrectly - KurtosisPackageIndexerClient is not currently available.`,
);

return client;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// @generated by protoc-gen-connect-es v0.13.2 with parameter "target=ts"
// @generated from file kurtosis_package_indexer.proto (package kurtosis_package_indexer, syntax proto3)
/* eslint-disable */
// @ts-nocheck

import { Empty, MethodKind } from "@bufbuild/protobuf";
import { GetPackagesResponse, ReadPackageRequest, ReadPackageResponse } from "./kurtosis_package_indexer_pb";

/**
* @generated from service kurtosis_package_indexer.KurtosisPackageIndexer
*/
export const KurtosisPackageIndexer = {
typeName: "kurtosis_package_indexer.KurtosisPackageIndexer",
methods: {
/**
* @generated from rpc kurtosis_package_indexer.KurtosisPackageIndexer.IsAvailable
*/
isAvailable: {
name: "IsAvailable",
I: Empty,
O: Empty,
kind: MethodKind.Unary,
},
/**
* @generated from rpc kurtosis_package_indexer.KurtosisPackageIndexer.GetPackages
*/
getPackages: {
name: "GetPackages",
I: Empty,
O: GetPackagesResponse,
kind: MethodKind.Unary,
},
/**
* @generated from rpc kurtosis_package_indexer.KurtosisPackageIndexer.Reindex
*/
reindex: {
name: "Reindex",
I: Empty,
O: Empty,
kind: MethodKind.Unary,
},
/**
* @generated from rpc kurtosis_package_indexer.KurtosisPackageIndexer.ReadPackage
*/
readPackage: {
name: "ReadPackage",
I: ReadPackageRequest,
O: ReadPackageResponse,
kind: MethodKind.Unary,
},
},
} as const;
Loading

0 comments on commit b0af9d8

Please sign in to comment.