Kaynağa Gözat

new preflight check endpoint/handler for AWS/Azure (#4331)

Feroze Mohideen 2 yıl önce
ebeveyn
işleme
b2f023f508

+ 118 - 0
api/server/handlers/api_contract/preflight.go

@@ -0,0 +1,118 @@
+package api_contract
+
+import (
+	"net/http"
+
+	"connectrpc.com/connect"
+	"github.com/porter-dev/api-contracts/generated/go/helpers"
+	porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/internal/telemetry"
+)
+
+// PreflightCheckHandler runs preflight checks on a cluster contract
+type PreflightCheckHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+// NewPreflightCheckHandler returns a new PreflightCheckHandler
+func NewPreflightCheckHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *PreflightCheckHandler {
+	return &PreflightCheckHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+// PorterError is the error response for the preflight check endpoint
+type PorterError struct {
+	Code     string            `json:"code"`
+	Message  string            `json:"message"`
+	Metadata map[string]string `json:"metadata,omitempty"`
+}
+
+// PreflightCheckError is the error response for the preflight check endpoint
+type PreflightCheckError struct {
+	Name  string      `json:"name"`
+	Error PorterError `json:"error"`
+}
+
+// PreflightCheckResponse is the response to the preflight check endpoint
+type PreflightCheckResponse struct {
+	Errors []PreflightCheckError `json:"errors"`
+}
+
+var recognizedPreflightCheckTypes = []string{
+	"eip",
+	"vcpu",
+	"vpc",
+	"natGateway",
+	"apiEnabled",
+	"cidrAvailability",
+	"iamPermissions",
+	"authz",
+}
+
+func (p *PreflightCheckHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx, span := telemetry.NewSpan(r.Context(), "serve-preflight-checks")
+	defer span.End()
+
+	var apiContract porterv1.Contract
+
+	err := helpers.UnmarshalContractObjectFromReader(r.Body, &apiContract)
+	if err != nil {
+		e := telemetry.Error(ctx, span, err, "error parsing api contract")
+		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(e, http.StatusBadRequest))
+		return
+	}
+
+	var resp PreflightCheckResponse
+
+	req := porterv1.CloudContractPreflightCheckRequest{
+		Contract: &apiContract,
+	}
+
+	checkResp, err := p.Config().ClusterControlPlaneClient.CloudContractPreflightCheck(ctx, connect.NewRequest(&req))
+	if err != nil {
+		err = telemetry.Error(ctx, span, err, "error calling preflight checks")
+		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	if checkResp.Msg == nil {
+		err = telemetry.Error(ctx, span, nil, "no message received from preflight checks")
+		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	errors := []PreflightCheckError{}
+	for _, check := range checkResp.Msg.FailingPreflightChecks {
+		if check.Message == "" || !contains(recognizedPreflightCheckTypes, check.Type) {
+			continue
+		}
+
+		errors = append(errors, PreflightCheckError{
+			Name: check.Type,
+			Error: PorterError{
+				Message:  check.Message,
+				Metadata: check.Metadata,
+			},
+		})
+	}
+	resp.Errors = errors
+	p.WriteResult(w, r, resp)
+}
+
+func contains(slice []string, elem string) bool {
+	for _, item := range slice {
+		if item == elem {
+			return true
+		}
+	}
+	return false
+}

+ 0 - 3
api/server/handlers/project_integration/preflight_check.go

@@ -1,7 +1,6 @@
 package project_integration
 
 import (
-	"fmt"
 	"net/http"
 
 	"connectrpc.com/connect"
@@ -119,8 +118,6 @@ func (p *CreatePreflightCheckHandler) ServeHTTP(w http.ResponseWriter, r *http.R
 				continue
 			}
 
-			fmt.Printf("val: %+v\n", val.Metadata)
-
 			errors = append(errors, PreflightCheckError{
 				Name: val.Type,
 				Error: PorterError{

+ 28 - 0
api/server/router/project.go

@@ -1543,6 +1543,34 @@ func getProjectRoutes(
 		Router:   r,
 	})
 
+	// POST /api/projects/{project_id}/contract/preflight -> apiContract.NewPreflightCheckHandler
+	preflightCheckEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbCreate,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("%s/contract/preflight", relPath),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+			},
+		},
+	)
+
+	preflightCheckHandler := apiContract.NewPreflightCheckHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: preflightCheckEndpoint,
+		Handler:  preflightCheckHandler,
+		Router:   r,
+	})
+
 	// POST /api/projects/{project_id}/rename -> cluster.newRenamProject
 	renameProjectEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{

+ 1 - 1
dashboard/src/components/GCPProvisionerSettings.tsx

@@ -545,7 +545,7 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
           })
         }
       });
-      const preflightDataResp = await api.preflightCheck(
+      const preflightDataResp = await api.legacyPreflightCheck(
         "<token>", data,
         {
           id: currentProject.id,

+ 1 - 1
dashboard/src/components/ProvisionerSettings.tsx

@@ -686,7 +686,7 @@ const ProvisionerSettings: React.FC<Props> = (props) => {
       const data = new PreflightCheckRequest({
         contract,
       });
-      const preflightDataResp = await api.preflightCheck("<token>", data, {
+      const preflightDataResp = await api.legacyPreflightCheck("<token>", data, {
         id: currentProject.id,
       });
       // Check if any of the preflight checks has a message

+ 13 - 13
dashboard/src/lib/clusters/constants.ts

@@ -1104,6 +1104,9 @@ const AWS_VCPUS_QUOTA_RESOLUTION: PreflightCheckResolution = {
   subtitle:
     "You will need to either request more vCPUs or delete existing instances in order to provision in the region specified. You can request more vCPUs by following these steps:",
   steps: [
+    {
+      text: "Note the quota name flagged in the provision check error as well as the additional quota value required.",
+    },
     {
       text: "Log in to your AWS Account",
       externalLink:
@@ -1132,16 +1135,15 @@ const AZURE_AUTHZ_RESOLUTION: PreflightCheckResolution = {
   steps: [
     {
       text: "Log in to your Azure Portal:",
-      externalLink:
-        "https://portal.azure.com",
+      externalLink: "https://portal.azure.com",
     },
     {
-      text: "Click on the Azure Cloud Shell icon to the right of the global search bar, and select Bash as your shell"
+      text: "Click on the Azure Cloud Shell icon to the right of the global search bar, and select Bash as your shell",
     },
     {
       text: "Follow the directions in our docs to create the Azure role required for provisioning with Porter and attach it to a service principal:",
       externalLink:
-          "https://docs.porter.run/provision/provisioning-on-azure#creating-the-service-principal",
+        "https://docs.porter.run/provision/provisioning-on-azure#creating-the-service-principal",
     },
     {
       text: "Note the outputted credentials, return to the Azure credential input screen in Porter, and re-enter the credentials to provision",
@@ -1154,17 +1156,16 @@ const AZURE_RESOURCE_PROVIDER_RESOLUTION: PreflightCheckResolution = {
     "You will need to enable certain resource providers in your Azure subscription in order for Porter to provision your infrastructure:",
   steps: [
     {
-      text: "Take note of any particular resource providers flagged as missing in the provisioning error message."
+      text: "Take note of any particular resource providers flagged as missing in the provisioning error message.",
     },
     {
       text: "Log in to your Azure Portal:",
-      externalLink:
-          "https://portal.azure.com",
+      externalLink: "https://portal.azure.com",
     },
     {
       text: "Follow the directions in our docs to enable all required resource providers in your subscription:",
       externalLink:
-          "https://docs.porter.run/provision/provisioning-on-azure#prerequisites",
+        "https://docs.porter.run/provision/provisioning-on-azure#prerequisites",
     },
     {
       text: "Changes may take a few minutes to take effect. Once you have enabled the resource providers, return to Porter and retry the provision.",
@@ -1174,20 +1175,19 @@ const AZURE_RESOURCE_PROVIDER_RESOLUTION: PreflightCheckResolution = {
 const AZURE_VCPUS_QUOTA_RESOLUTION: PreflightCheckResolution = {
   title: "Requesting more vCPUs",
   subtitle:
-      "You will need to either request more vCPUs or delete existing instances in order to provision in the location specified. You can request more vCPUs by following these steps:",
+    "You will need to either request more vCPUs or delete existing instances in order to provision in the location specified. You can request more vCPUs by following these steps:",
   steps: [
     {
-      text: "Note which resource families were flagged in the provisioning error message. These may include your requested machine types, as well as those required by Porter."
+      text: "Note which resource families were flagged in the provisioning error message. These may include your requested machine types, as well as those required by Porter.",
     },
     {
       text: "Log in to your Azure Portal:",
-      externalLink:
-          "https://portal.azure.com",
+      externalLink: "https://portal.azure.com",
     },
     {
       text: "Follow the directions in our docs to request quota increases:",
       externalLink:
-          "https://docs.porter.run/provision/provisioning-on-azure#compute-quotas",
+        "https://docs.porter.run/provision/provisioning-on-azure#compute-quotas",
     },
     {
       text: "Requests may take a few hours to be fulfilled. Once you have confirmed that the quota increases have been granted, return to Porter and retry the provision.",

+ 27 - 12
dashboard/src/lib/hooks/useCluster.ts

@@ -10,9 +10,10 @@ import {
   updateExistingClusterContract,
 } from "lib/clusters";
 import {
-    CloudProviderAWS, CloudProviderAzure,
-    CloudProviderGCP,
-    SUPPORTED_CLOUD_PROVIDERS,
+  CloudProviderAWS,
+  CloudProviderAzure,
+  CloudProviderGCP,
+  SUPPORTED_CLOUD_PROVIDERS,
 } from "lib/clusters/constants";
 import {
   clusterStateValidator,
@@ -363,15 +364,29 @@ export const useUpdateCluster = ({
 
     setIsHandlingPreflightChecks(true);
     try {
-      const preflightCheckResp = await api.preflightCheck(
-        "<token>",
-        new PreflightCheckRequest({
-          contract: newContract,
-        }),
-        {
-          id: projectId,
-        }
-      );
+      let preflightCheckResp;
+      if (
+        clientContract.cluster.cloudProvider === "AWS" ||
+        clientContract.cluster.cloudProvider === "Azure"
+      ) {
+        preflightCheckResp = await api.cloudContractPreflightCheck(
+          "<token>",
+          newContract,
+          {
+            project_id: projectId,
+          }
+        );
+      } else {
+        preflightCheckResp = await api.legacyPreflightCheck(
+          "<token>",
+          new PreflightCheckRequest({
+            contract: newContract,
+          }),
+          {
+            id: projectId,
+          }
+        );
+      }
       const parsed = await preflightCheckValidator.parseAsync(
         preflightCheckResp.data
       );

+ 16 - 6
dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/GPUResources.tsx

@@ -1,9 +1,11 @@
 import React, { useMemo, useState } from "react";
 import { Switch } from "@material-ui/core";
 import { Controller, useFormContext } from "react-hook-form";
+import { useHistory } from "react-router";
 import styled from "styled-components";
 
 import Loading from "components/Loading";
+import Button from "components/porter/Button";
 import Container from "components/porter/Container";
 import Link from "components/porter/Link";
 import Modal from "components/porter/Modal";
@@ -23,6 +25,8 @@ type Props = {
 
 // TODO: allow users to provision multiple GPU nodes in the slider
 const GPUResources: React.FC<Props> = ({ index, cluster }) => {
+  const history = useHistory();
+
   const [clusterModalVisible, setClusterModalVisible] =
     useState<boolean>(false);
 
@@ -90,15 +94,21 @@ const GPUResources: React.FC<Props> = ({ index, cluster }) => {
               >
                 <div>
                   <Text size={16}>Cluster GPU check</Text>
-                  <Spacer height="15px" />
+                  <Spacer y={0.5} />
                   <Text color="helper">
                     Your cluster is not yet configured to allow applications to
-                    run on GPU nodes.
+                    run on GPU nodes. You can add a GPU node group in your
+                    infrastructure dashboard.
                   </Text>
-                  <Spacer height="15px" />
-                  <Link to={`/infrastructure/${cluster.id}`}>
-                    You can add a GPU node group to your cluster here.
-                  </Link>
+                  <Spacer y={1} />
+                  <Button
+                    alt
+                    onClick={() => {
+                      history.push(`/infrastructure/${cluster.id}`);
+                    }}
+                  >
+                    To infrastructure
+                  </Button>
                 </div>
               </Modal>
             )}

+ 5 - 1
dashboard/src/main/home/infrastructure-dashboard/modals/PreflightChecksModal.tsx

@@ -37,7 +37,9 @@ export const CheckItem: React.FC<ItemProps> = ({
         <Spacer inline x={1} />
         <Text style={{ flex: 1 }}>{preflightCheck.title}</Text>
         {preflightCheck?.error?.metadata?.quotaName && (
-          <Text color={"helper"}>{preflightCheck?.error?.metadata?.quotaName}</Text>
+          <Text color={"helper"}>
+            {preflightCheck?.error?.metadata?.quotaName}
+          </Text>
         )}
       </CheckItemTop>
     );
@@ -111,6 +113,8 @@ const AppearingDiv = styled.div`
   display: flex;
   flex-direction: column;
   color: #fff;
+  max-height: 80vh;
+  overflow-y: auto;
 
   @keyframes floatIn {
     from {

+ 10 - 2
dashboard/src/shared/api.tsx

@@ -80,7 +80,7 @@ const getGitlabIntegration = baseApi<{}, { project_id: number }>(
   ({ project_id }) => `/api/projects/${project_id}/integrations/gitlab`
 );
 
-const preflightCheck = baseApi<PreflightCheckRequest, { id: number }>(
+const legacyPreflightCheck = baseApi<PreflightCheckRequest, { id: number }>(
   "POST",
   (pathParams) => {
     return `/api/projects/${pathParams.id}/integrations/preflightcheck`;
@@ -1562,6 +1562,13 @@ const createContract = baseApi<Contract, { project_id: number }>(
   }
 );
 
+const cloudContractPreflightCheck = baseApi<Contract, { project_id: number }>(
+  "POST",
+  ({ project_id }) => {
+    return `/api/projects/${project_id}/contract/preflight`;
+  }
+);
+
 const getContracts = baseApi<
   { cluster_id?: number; latest?: boolean },
   { project_id: number }
@@ -3678,7 +3685,7 @@ export default {
   addApplicationToEnvGroup,
   removeApplicationFromEnvGroup,
   provisionDatabase,
-  preflightCheck,
+  legacyPreflightCheck,
   requestQuotaIncrease,
   getAwsCloudProviders,
   getDatabases,
@@ -3713,6 +3720,7 @@ export default {
   getIncidentEvents,
   createContract,
   getContracts,
+  cloudContractPreflightCheck,
   deleteContract,
   // TRACKING
   updateOnboardingStep,