| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532 |
- import { useContext, useState } from "react";
- import { Contract, PreflightCheckRequest } from "@porter-dev/api-contracts";
- import { useQuery } from "@tanstack/react-query";
- import axios from "axios";
- import { match } from "ts-pattern";
- import { z } from "zod";
- import {
- clientClusterContractFromProto,
- updateExistingClusterContract,
- } from "lib/clusters";
- import {
- CloudProviderAWS, CloudProviderAzure,
- CloudProviderGCP,
- SUPPORTED_CLOUD_PROVIDERS,
- } from "lib/clusters/constants";
- import {
- clusterStateValidator,
- clusterValidator,
- contractValidator,
- createContractResponseValidator,
- nodeValidator,
- preflightCheckValidator,
- type APIContract,
- type ClientCluster,
- type ClientClusterContract,
- type ClientNode,
- type ClientPreflightCheck,
- type ClusterState,
- type ContractCondition,
- type UpdateClusterResponse,
- } from "lib/clusters/types";
- import api from "shared/api";
- import { Context } from "shared/Context";
- import { valueExists } from "shared/util";
- type TUseClusterList = {
- clusters: ClientCluster[];
- isLoading: boolean;
- };
- export const useClusterList = (): TUseClusterList => {
- const { currentProject } = useContext(Context);
- const clusterReq = useQuery(
- ["getClusters", currentProject?.id],
- async () => {
- if (!currentProject?.id || currentProject.id === -1) {
- return;
- }
- const res = await api.getClusters(
- "<token>",
- {},
- { id: currentProject.id }
- );
- const parsed = await z.array(clusterValidator).parseAsync(res.data);
- const filtered = parsed
- .map((c) => {
- const cloudProviderMatch = SUPPORTED_CLOUD_PROVIDERS.find(
- (s) => s.name === c.cloud_provider
- );
- return cloudProviderMatch
- ? { ...c, cloud_provider: cloudProviderMatch }
- : null;
- })
- .filter(valueExists);
- const latestContractsRes = await api.getContracts(
- "<token>",
- { latest: true },
- { project_id: currentProject.id }
- );
- const latestContracts = await z
- .array(contractValidator)
- .parseAsync(latestContractsRes.data);
- return filtered
- .map((c) => {
- const latestContract = latestContracts.find(
- (contract) => contract.cluster_id === c.id
- );
- // if this cluster has no latest contract, don't include it
- if (!latestContract) {
- return undefined;
- }
- const latestClientContract = clientClusterContractFromProto(
- Contract.fromJsonString(atob(latestContract.base64_contract), {
- ignoreUnknownFields: true,
- })
- );
- // if we can't parse the latest contract, don't include it
- if (!latestClientContract) {
- return undefined;
- }
- return {
- ...c,
- contract: {
- ...latestContract,
- config: latestClientContract,
- },
- };
- })
- .filter(valueExists);
- },
- {
- enabled: !!currentProject && currentProject.id !== -1,
- }
- );
- return {
- clusters: clusterReq.data ?? [],
- isLoading: clusterReq.isLoading,
- };
- };
- type TUseCluster = {
- cluster: ClientCluster | undefined;
- isLoading: boolean;
- isError: boolean;
- };
- export const useCluster = ({
- clusterId,
- refetchInterval,
- }: {
- clusterId: number | undefined;
- refetchInterval?: number;
- }): TUseCluster => {
- const { currentProject } = useContext(Context);
- const clusterReq = useQuery(
- ["getCluster", currentProject?.id, clusterId],
- async () => {
- if (
- !currentProject?.id ||
- currentProject.id === -1 ||
- !clusterId ||
- clusterId === -1
- ) {
- return;
- }
- // get the cluster + match with what we know
- const res = await api.getCluster(
- "<token>",
- {},
- { project_id: currentProject.id, cluster_id: clusterId }
- );
- const parsed = await clusterValidator.parseAsync(res.data);
- const cloudProviderMatch = SUPPORTED_CLOUD_PROVIDERS.find(
- (s) => s.name === parsed.cloud_provider
- );
- if (!cloudProviderMatch) {
- return;
- }
- // get the latest contract
- const latestContractsRes = await api.getContracts(
- "<token>",
- { latest: true, cluster_id: clusterId },
- { project_id: currentProject.id }
- );
- const latestContracts = await z
- .array(contractValidator)
- .parseAsync(latestContractsRes.data);
- if (latestContracts.length !== 1) {
- return;
- }
- const latestClientContract = clientClusterContractFromProto(
- Contract.fromJsonString(atob(latestContracts[0].base64_contract), {
- ignoreUnknownFields: true,
- })
- );
- if (!latestClientContract) {
- return;
- }
- // get the latest state
- const stateRes = await api.getClusterState(
- "<token>",
- {},
- { project_id: currentProject.id, cluster_id: clusterId }
- );
- const state = await clusterStateValidator.parseAsync(stateRes.data);
- return {
- ...parsed,
- cloud_provider: cloudProviderMatch,
- state,
- contract: {
- ...latestContracts[0],
- config: latestClientContract,
- },
- };
- },
- {
- enabled:
- !!currentProject &&
- currentProject.id !== -1 &&
- !!clusterId &&
- clusterId !== -1,
- refetchInterval,
- }
- );
- return {
- cluster: clusterReq.data,
- isLoading: clusterReq.isLoading,
- isError: clusterReq.isError,
- };
- };
- export const useLatestClusterContract = ({
- clusterId,
- }: {
- clusterId: number | undefined;
- }): {
- contractDB: APIContract | undefined;
- contractProto: Contract | undefined;
- clientContract: ClientClusterContract | undefined;
- clusterCondition: ContractCondition | undefined;
- isLoading: boolean;
- isError: boolean;
- } => {
- const { currentProject } = useContext(Context);
- const latestClusterContractReq = useQuery(
- ["getLatestClusterContract", currentProject?.id, clusterId],
- async () => {
- if (
- !currentProject?.id ||
- currentProject.id === -1 ||
- !clusterId ||
- clusterId === -1
- ) {
- return;
- }
- const res = await api.getContracts(
- "<token>",
- { cluster_id: clusterId, latest: true },
- { project_id: currentProject.id }
- );
- const data = await z.array(contractValidator).parseAsync(res.data);
- if (data.length !== 1) {
- return;
- }
- const contractDB = data[0];
- const contractProto = Contract.fromJsonString(
- atob(contractDB.base64_contract),
- {
- ignoreUnknownFields: true,
- }
- );
- const clientContract = clientClusterContractFromProto(contractProto);
- return {
- contractDB,
- contractProto,
- clientContract,
- clusterCondition: contractDB.condition,
- };
- },
- {
- refetchInterval: 3000,
- enabled:
- !!currentProject &&
- currentProject.id !== -1 &&
- !!clusterId &&
- clusterId !== -1,
- }
- );
- return {
- contractDB: latestClusterContractReq.data?.contractDB,
- contractProto: latestClusterContractReq.data?.contractProto,
- clientContract: latestClusterContractReq.data?.clientContract,
- clusterCondition: latestClusterContractReq.data?.clusterCondition,
- isLoading: latestClusterContractReq.isLoading,
- isError: latestClusterContractReq.isError,
- };
- };
- type TUseClusterState = {
- state: ClusterState | undefined;
- isLoading: boolean;
- isError: boolean;
- };
- export const useClusterState = ({
- clusterId,
- }: {
- clusterId: number | undefined;
- }): TUseClusterState => {
- const { currentProject } = useContext(Context);
- const clusterStateReq = useQuery(
- ["getClusterState", currentProject?.id, clusterId],
- async () => {
- if (
- !currentProject?.id ||
- currentProject.id === -1 ||
- !clusterId ||
- clusterId === -1
- ) {
- return;
- }
- const res = await api.getClusterState(
- "<token>",
- {},
- { project_id: currentProject.id, cluster_id: clusterId }
- );
- const parsed = await clusterStateValidator.parseAsync(res.data);
- return parsed;
- },
- {
- enabled:
- !!currentProject &&
- currentProject.id !== -1 &&
- !!clusterId &&
- clusterId !== -1,
- refetchInterval: 5000,
- }
- );
- return {
- state: clusterStateReq.data,
- isLoading: clusterStateReq.isLoading,
- isError: clusterStateReq.isError,
- };
- };
- type TUseUpdateCluster = {
- updateCluster: (
- clientContract: ClientClusterContract,
- baseContract: Contract
- ) => Promise<UpdateClusterResponse>;
- isHandlingPreflightChecks: boolean;
- isCreatingContract: boolean;
- };
- export const useUpdateCluster = ({
- projectId,
- }: {
- projectId: number | undefined;
- }): TUseUpdateCluster => {
- const [isHandlingPreflightChecks, setIsHandlingPreflightChecks] =
- useState<boolean>(false);
- const [isCreatingContract, setIsCreatingContract] = useState<boolean>(false);
- const updateCluster = async (
- clientContract: ClientClusterContract,
- baseContract: Contract
- ): Promise<UpdateClusterResponse> => {
- if (!projectId) {
- throw new Error("Project ID is missing");
- }
- if (!baseContract.cluster) {
- throw new Error("Cluster is missing");
- }
- const newContract = new Contract({
- ...baseContract,
- cluster: updateExistingClusterContract(
- clientContract,
- baseContract.cluster
- ),
- });
- setIsHandlingPreflightChecks(true);
- try {
- const preflightCheckResp = await api.preflightCheck(
- "<token>",
- new PreflightCheckRequest({
- contract: newContract,
- }),
- {
- id: projectId,
- }
- );
- const parsed = await preflightCheckValidator.parseAsync(
- preflightCheckResp.data
- );
- if (parsed.errors.length > 0) {
- const cloudProviderSpecificChecks = match(
- clientContract.cluster.cloudProvider
- )
- .with("AWS", () => CloudProviderAWS.preflightChecks)
- .with("GCP", () => CloudProviderGCP.preflightChecks)
- .with("Azure", () => CloudProviderAzure.preflightChecks)
- .otherwise(() => []);
- const clientPreflightChecks: ClientPreflightCheck[] = parsed.errors
- .map((e) => {
- const preflightCheckMatch = cloudProviderSpecificChecks.find(
- (cloudProviderCheck) => e.name === cloudProviderCheck.name
- );
- if (!preflightCheckMatch) {
- return undefined;
- }
- return {
- title: preflightCheckMatch.displayName,
- status: "failure" as const,
- error: {
- detail: e.error.message,
- metadata: e.error.metadata,
- resolution: preflightCheckMatch.resolution,
- },
- };
- })
- .filter(valueExists);
- return {
- preflightChecks: clientPreflightChecks,
- };
- }
- // otherwise, continue to create the contract
- } catch (err) {
- throw new Error(
- getErrorMessageFromNetworkCall(err, "Cluster preflight checks")
- );
- } finally {
- setIsHandlingPreflightChecks(false);
- }
- setIsCreatingContract(true);
- try {
- const createContractResp = await api.createContract(
- "<token>",
- newContract,
- {
- project_id: projectId,
- }
- );
- const parsed = await createContractResponseValidator.parseAsync(
- createContractResp.data
- );
- return {
- createContractResponse: parsed,
- };
- } catch (err) {
- throw new Error(getErrorMessageFromNetworkCall(err, "Cluster creation"));
- } finally {
- setIsCreatingContract(false);
- }
- };
- return {
- updateCluster,
- isHandlingPreflightChecks,
- isCreatingContract,
- };
- };
- type TUseClusterNodeList = {
- nodes: ClientNode[];
- isLoading: boolean;
- };
- export const useClusterNodeList = ({
- clusterId,
- refetchInterval = 3000,
- }: {
- clusterId: number | undefined;
- refetchInterval?: number;
- }): TUseClusterNodeList => {
- const { currentProject } = useContext(Context);
- const clusterNodesReq = useQuery(
- ["getClusterNodes", currentProject?.id, clusterId],
- async () => {
- if (
- !currentProject?.id ||
- currentProject.id === -1 ||
- !clusterId ||
- clusterId === -1
- ) {
- return;
- }
- const res = await api.getClusterNodes(
- "<token>",
- {},
- { project_id: currentProject.id, cluster_id: clusterId }
- );
- const parsed = await z.array(nodeValidator).parseAsync(res.data);
- return parsed
- .map((n) => {
- const nodeGroupType = match(n.labels["porter.run/workload-kind"])
- .with("application", () => "APPLICATION" as const)
- .with("system", () => "SYSTEM" as const)
- .with("monitoring", () => "MONITORING" as const)
- .with("custom", () => "CUSTOM" as const)
- .otherwise(() => "UNKNOWN" as const);
- if (nodeGroupType === "UNKNOWN") {
- return undefined;
- }
- const instanceType = n.labels["node.kubernetes.io/instance-type"];
- if (!instanceType) {
- return undefined;
- }
- return {
- nodeGroupType,
- instanceType,
- };
- })
- .filter(valueExists);
- },
- {
- refetchInterval,
- enabled:
- !!currentProject &&
- currentProject.id !== -1 &&
- !!clusterId &&
- clusterId !== -1,
- }
- );
- return {
- nodes: clusterNodesReq.data ?? [],
- isLoading: clusterNodesReq.isLoading,
- };
- };
- const getErrorMessageFromNetworkCall = (
- err: unknown,
- networkCallDescription: string
- ): string => {
- if (axios.isAxiosError(err)) {
- const parsed = z
- .object({ error: z.string() })
- .safeParse(err.response?.data);
- if (parsed.success) {
- return `${networkCallDescription} failed: ${parsed.data.error}`;
- }
- }
- return `${networkCallDescription} failed: please try again or contact support@porter.run if the error persists.`;
- };
|