Prechádzať zdrojové kódy

move validation to hook and add commit sha (#3422)

ianedwards 2 rokov pred
rodič
commit
8865b56ff4

+ 114 - 0
dashboard/src/lib/hooks/useAppValidation.ts

@@ -0,0 +1,114 @@
+import { PorterApp } from "@porter-dev/api-contracts";
+import {
+  PorterAppFormData,
+  SourceOptions,
+  clientAppToProto,
+} from "lib/porter-apps";
+import { useCallback, useContext } from "react";
+import { Context } from "shared/Context";
+import api from "shared/api";
+import { match } from "ts-pattern";
+import { z } from "zod";
+
+export const useAppValidation = ({
+  deploymentTargetID,
+}: {
+  deploymentTargetID?: string;
+}) => {
+  const { currentProject, currentCluster } = useContext(Context);
+
+  const getBranchHead = async ({
+    projectID,
+    source,
+  }: {
+    projectID: number;
+    source: SourceOptions & {
+      type: "github";
+    };
+  }) => {
+    const [owner, repo_name] = await z
+      .tuple([z.string(), z.string()])
+      .parseAsync(source.git_repo_name?.split("/"));
+
+    const res = await api.getBranchHead(
+      "<token>",
+      {},
+      {
+        ...source,
+        project_id: projectID,
+        kind: "github",
+        owner,
+        name: repo_name,
+        branch: source.git_branch,
+      }
+    );
+
+    const commitData = await z
+      .object({
+        commit_sha: z.string(),
+      })
+      .parseAsync(res.data);
+
+    return commitData;
+  };
+
+  const validateApp = useCallback(
+    async (data: PorterAppFormData) => {
+      try {
+        if (!currentProject || !currentCluster) {
+          throw new Error("No project or cluster selected");
+        }
+
+        if (!deploymentTargetID) {
+          throw new Error("No deployment target selected");
+        }
+
+        const proto = clientAppToProto(data);
+        const commit_sha = await match(data.source)
+          .with({ type: "github" }, async (src) => {
+            const { commit_sha } = await getBranchHead({
+              projectID: currentProject.id,
+              source: src,
+            });
+            return commit_sha;
+          })
+          .with({ type: "docker-registry" }, () => {
+            return "";
+          })
+          .exhaustive();
+
+        const res = await api.validatePorterApp(
+          "<token>",
+          {
+            b64_app_proto: btoa(proto.toJsonString()),
+            deployment_target_id: deploymentTargetID,
+            commit_sha,
+          },
+          {
+            project_id: currentProject.id,
+            cluster_id: currentCluster.id,
+          }
+        );
+
+        const validAppData = await z
+          .object({
+            validate_b64_app_proto: z.string(),
+          })
+          .parseAsync(res.data);
+
+        const validatedAppProto = PorterApp.fromJsonString(
+          atob(validAppData.validate_b64_app_proto)
+        );
+
+        return validatedAppProto;
+      } catch (err) {
+        return null;
+      }
+    },
+    [deploymentTargetID, currentProject, currentCluster]
+  );
+
+  return {
+    validateApp,
+  };
+};

+ 10 - 40
dashboard/src/main/home/app-dashboard/create-app/CreateApp.tsx

@@ -15,12 +15,7 @@ import { ControlledInput } from "components/porter/ControlledInput";
 import Link from "components/porter/Link";
 
 import { Context } from "shared/Context";
-import {
-  PorterAppFormData,
-  SourceOptions,
-  clientAppToProto,
-  porterAppFormValidator,
-} from "lib/porter-apps";
+import { PorterAppFormData, SourceOptions, porterAppFormValidator } from "lib/porter-apps";
 import DashboardHeader from "main/home/cluster-dashboard/DashboardHeader";
 import SourceSelector from "../new-app-flow/SourceSelector";
 import Button from "components/porter/Button";
@@ -36,13 +31,14 @@ import EnvVariables from "../validate-apply/app-settings/EnvVariables";
 import { usePorterYaml } from "lib/hooks/usePorterYaml";
 import { valueExists } from "shared/util";
 import api from "shared/api";
-import { z } from "zod";
 import { PorterApp } from "@porter-dev/api-contracts";
 import GithubActionModal from "../new-app-flow/GithubActionModal";
 import { useDefaultDeploymentTarget } from "lib/hooks/useDeploymentTarget";
 import Error from "components/porter/Error";
 import { useAppAnalytics } from "lib/hooks/useAppAnalytics";
+import { useAppValidation } from "lib/hooks/useAppValidation";
 import { useQuery } from "@tanstack/react-query";
+import { z } from "zod";
 
 type CreateAppProps = {} & RouteComponentProps;
 
@@ -129,42 +125,15 @@ const CreateApp: React.FC<CreateAppProps> = ({ history }) => {
   const servicesFromYaml = usePorterYaml(source);
   const deploymentTarget = useDefaultDeploymentTarget();
   const { updateAppStep } = useAppAnalytics(name);
+  const { validateApp } = useAppValidation({
+    deploymentTargetID: deploymentTarget?.deployment_target_id,
+  });
 
   const onSubmit = handleSubmit(async (data) => {
     try {
-      if (!currentProject || !currentCluster) {
-        return;
-      }
-
-      if (!deploymentTarget) {
-        return;
-      }
-
-      const proto = clientAppToProto(data);
-      const res = await api.validatePorterApp(
-        "<token>",
-        {
-          b64_app_proto: btoa(proto.toJsonString()),
-          deployment_target_id: deploymentTarget.deployment_target_id,
-          commit_sha: "",
-        },
-        {
-          project_id: currentProject.id,
-          cluster_id: currentCluster.id,
-        }
-      );
-
-      const validAppData = await z
-        .object({
-          validate_b64_app_proto: z.string(),
-        })
-        .parseAsync(res.data);
-
-      const validatedAppProto = PorterApp.fromJsonString(
-        atob(validAppData.validate_b64_app_proto)
-      );
-
+      const validatedAppProto = await validateApp(data);
       setValidatedAppProto(validatedAppProto);
+
       if (source?.type === "github") {
         setShowGHAModal(true);
         return;
@@ -365,8 +334,9 @@ const CreateApp: React.FC<CreateAppProps> = ({ history }) => {
       setError("app.name", {
         message: "An app with this name already exists",
       });
+      return
     }
-  }, [porterApps]);
+  }, [porterApps, name]);
 
   if (!currentProject || !currentCluster) {
     return null;

+ 19 - 0
dashboard/src/shared/api.tsx

@@ -803,6 +803,24 @@ const getDefaultDeploymentTarget = baseApi<
   return `/api/projects/${pathParams.project_id}/clusters/${pathParams.cluster_id}/default-deployment-target`;
 });
 
+const getBranchHead = baseApi<
+  {},
+  {
+    project_id: number;
+    git_repo_id: number;
+    kind: string;
+    owner: string;
+    name: string;
+    branch: string;
+  }
+>("GET", (pathParams) => {
+  return `/api/projects/${pathParams.project_id}/gitrepos/${
+    pathParams.git_repo_id
+  }/repos/${pathParams.kind}/${pathParams.owner}/${
+    pathParams.name
+  }/${encodeURIComponent(pathParams.branch)}/head`;
+});
+
 const validatePorterApp = baseApi<
   {
     b64_app_proto: string;
@@ -2937,6 +2955,7 @@ export default {
   getPorterYamlContents,
   parsePorterYaml,
   getDefaultDeploymentTarget,
+  getBranchHead,
   validatePorterApp,
   createApp,
   applyApp,