diff --git a/.github/golangci-lint.yaml b/.github/golangci-lint.yaml index a775708451..05fe6d917d 100644 --- a/.github/golangci-lint.yaml +++ b/.github/golangci-lint.yaml @@ -64,4 +64,4 @@ output: path-prefix: "" # sorts results by: filepath, line and column - sort-results: false \ No newline at end of file + sort-results: false diff --git a/api/server/handlers/project_integration/preflight_check.go b/api/server/handlers/project_integration/preflight_check.go index 986f895750..4be68bbce9 100644 --- a/api/server/handlers/project_integration/preflight_check.go +++ b/api/server/handlers/project_integration/preflight_check.go @@ -1,6 +1,7 @@ package project_integration import ( + "fmt" "net/http" "connectrpc.com/connect" @@ -57,7 +58,7 @@ var recognizedPreflightCheckKeys = []string{ "apiEnabled", "cidrAvailability", "iamPermissions", - "resourceProviders", + "authz", } func (p *CreatePreflightCheckHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -91,6 +92,48 @@ func (p *CreatePreflightCheckHandler) ServeHTTP(w http.ResponseWriter, r *http.R } } + if cloudValues.Contract != nil && cloudValues.Contract.Cluster != nil && cloudValues.Contract.Cluster.CloudProvider == porterv1.EnumCloudProvider_ENUM_CLOUD_PROVIDER_AZURE { + telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "new-endpoint", Value: true}) + checkResp, err := p.Config().ClusterControlPlaneClient.CloudContractPreflightCheck(ctx, + connect.NewRequest( + &porterv1.CloudContractPreflightCheckRequest{ + Contract: cloudValues.Contract, + }, + ), + ) + 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 _, val := range checkResp.Msg.FailingPreflightChecks { + if val.Message == "" || !contains(recognizedPreflightCheckKeys, val.Type) { + continue + } + + fmt.Printf("val: %+v\n", val.Metadata) + + errors = append(errors, PreflightCheckError{ + Name: val.Type, + Error: PorterError{ + Message: val.Message, + Metadata: val.Metadata, + }, + }) + } + resp.Errors = errors + p.WriteResult(w, r, resp) + return + } + checkResp, err := p.Config().ClusterControlPlaneClient.PreflightCheck(ctx, connect.NewRequest(&input)) if err != nil { err = telemetry.Error(ctx, span, err, "error calling preflight checks") diff --git a/dashboard/src/components/porter/Error.tsx b/dashboard/src/components/porter/Error.tsx index d266269e51..52cb852501 100644 --- a/dashboard/src/components/porter/Error.tsx +++ b/dashboard/src/components/porter/Error.tsx @@ -3,9 +3,11 @@ import styled from "styled-components"; import Modal from "./Modal"; import Spacer from "./Spacer"; +import Text from "./Text"; type Props = { message: string; + metadata?: Record; ctaText?: string; ctaOnClick?: () => void; errorModalContents?: React.ReactNode; @@ -14,6 +16,7 @@ type Props = { export const Error: React.FC = ({ message, + metadata, ctaText, ctaOnClick, errorModalContents, @@ -26,10 +29,18 @@ export const Error: React.FC = ({ error_outline - Error: {message} + Error: {message} {ctaText && (errorModalContents != null || ctaOnClick != null) && ( <> + {metadata && + Object.entries(metadata).map(([key, value]) => ( +
+ {key}: + {value} +
+ ))} + { errorModalContents ? setErrorModalOpen(true) : ctaOnClick?.(); @@ -57,10 +68,6 @@ export const Error: React.FC = ({ export default Error; -const Text = styled.span` - display: inline; -`; - const Block = styled.div` display: block; `; @@ -101,3 +108,19 @@ const StyledError = styled.div<{ maxWidth?: string }>` } max-width: ${(props) => props.maxWidth || "100%"}; `; + +const ErrorMessageLabel = styled.span` + font-weight: bold; + margin-left: 10px; + color: #9999aa; + user-select: text; +`; +const ErrorMessageContent = styled.div` + font-family: "Courier New", Courier, monospace; + padding: 5px 10px; + border-radius: 4px; + margin-left: 10px; + user-select: text; + cursor: text; + color: #9999aa; +`; diff --git a/dashboard/src/components/porter/Link.tsx b/dashboard/src/components/porter/Link.tsx index e4002c96ca..40b8a4d5ef 100644 --- a/dashboard/src/components/porter/Link.tsx +++ b/dashboard/src/components/porter/Link.tsx @@ -59,9 +59,9 @@ const Underline = styled.div<{ color: string }>` background: ${(props) => props.color}; `; -const StyledLink = styled(DynamicLink) <{ hasunderline?: boolean, color: string }>` +const StyledLink = styled(DynamicLink) <{ hasunderline?: boolean, color: string, removeInline?: boolean }>` color: ${(props) => props.color}; - display: inline-flex; + ${(props) => !props.removeInline && "display: inline-flex;"}; font-size: 13px; cursor: pointer; align-items: center; diff --git a/dashboard/src/components/porter/Step.tsx b/dashboard/src/components/porter/Step.tsx index a9c7527f13..22bac2bf45 100644 --- a/dashboard/src/components/porter/Step.tsx +++ b/dashboard/src/components/porter/Step.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useState } from "react"; import styled from "styled-components"; +import Container from "./Container"; type Props = { number: number; @@ -13,19 +14,15 @@ const Step: React.FC = ({ return ( {number} - + {children} - + ); }; export default Step; -const Block = styled.div` - display: block; -`; - const StepNumber = styled.div` height: 20px; min-width: 20px; diff --git a/dashboard/src/lib/clusters/constants.ts b/dashboard/src/lib/clusters/constants.ts index 1feb7f934b..093574b19e 100644 --- a/dashboard/src/lib/clusters/constants.ts +++ b/dashboard/src/lib/clusters/constants.ts @@ -1027,7 +1027,7 @@ const AWS_EIP_QUOTA_RESOLUTION: PreflightCheckResolution = { "You will need to either request more EIP addresses or delete existing ones in order to provision in the region specified. You can request more addresses by following these steps:", steps: [ { - text: "Log into your AWS Account", + text: "Log in to your AWS Account", externalLink: "https://console.aws.amazon.com/billing/home?region=us-east-1#/account", }, @@ -1053,7 +1053,7 @@ const AWS_NAT_GATEWAY_QUOTA_RESOLUTION: PreflightCheckResolution = { "You will need to either request more NAT Gateways or delete existing ones in order to provision in the region specified. You can request more NAT Gateways by following these steps:", steps: [ { - text: "Log into your AWS Account", + text: "Log in to your AWS Account", externalLink: "https://console.aws.amazon.com/billing/home?region=us-east-1#/account", }, @@ -1079,7 +1079,7 @@ const AWS_VPC_QUOTA_RESOLUTION: PreflightCheckResolution = { "You will need to either request more VPCs or delete existing ones in order to provision in the region specified. You can request more VPCs by following these steps:", steps: [ { - text: "Log into your AWS Account", + text: "Log in to your AWS Account", externalLink: "https://console.aws.amazon.com/billing/home?region=us-east-1#/account", }, @@ -1105,7 +1105,7 @@ const AWS_VCPUS_QUOTA_RESOLUTION: PreflightCheckResolution = { "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: "Log into your AWS Account", + text: "Log in to your AWS Account", externalLink: "https://console.aws.amazon.com/billing/home?region=us-east-1#/account", }, @@ -1125,6 +1125,75 @@ const AWS_VCPUS_QUOTA_RESOLUTION: PreflightCheckResolution = { }, ], }; +const AZURE_AUTHZ_RESOLUTION: PreflightCheckResolution = { + title: "Granting your service principal authorization to your subscription", + subtitle: + "You will need to authorize your service principal to read and write to your subscription. To properly configure the service principal, following the creation steps in our docs:", + steps: [ + { + text: "Log in to your Azure Portal:", + 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: "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", + }, + { + text: "Note the outputted credentials, return to the Azure credential input screen in Porter, and re-enter the credentials to provision", + }, + ], +}; +const AZURE_RESOURCE_PROVIDER_RESOLUTION: PreflightCheckResolution = { + title: "Enable required resource providers in your subscription", + subtitle: + "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: "Log in to your Azure Portal:", + 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", + }, + { + text: "Changes may take a few minutes to take effect. Once you have enabled the resource providers, return to Porter and retry the provision.", + }, + ], +}; +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:", + 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: "Log in to your Azure Portal:", + 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", + }, + { + 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.", + }, + ], +}; const SUPPORTED_AWS_PREFLIGHT_CHECKS: PreflightCheck[] = [ { @@ -1149,6 +1218,24 @@ const SUPPORTED_AWS_PREFLIGHT_CHECKS: PreflightCheck[] = [ }, ]; +const SUPPORTED_AZURE_PREFLIGHT_CHECKS: PreflightCheck[] = [ + { + name: "authz", + displayName: "Subscription authorization", + resolution: AZURE_AUTHZ_RESOLUTION, + }, + { + name: "apiEnabled", + displayName: "Enable resource providers", + resolution: AZURE_RESOURCE_PROVIDER_RESOLUTION, + }, + { + name: "vcpu", + displayName: "vCPU availability", + resolution: AZURE_VCPUS_QUOTA_RESOLUTION, + }, +]; + const SUPPORTED_GCP_PREFLIGHT_CHECKS: PreflightCheck[] = [ { name: "apiEnabled", @@ -1254,7 +1341,7 @@ export const CloudProviderAzure: ClientCloudProvider & { machineTypes: SUPPORTED_AZURE_MACHINE_TYPES, baseCost: 164.69, newClusterDefaultContract: DEFAULT_AKS_CONTRACT, - preflightChecks: [], + preflightChecks: SUPPORTED_AZURE_PREFLIGHT_CHECKS, config: { kind: "Azure", skuTiers: SUPPORTED_AZURE_SKU_TIERS, diff --git a/dashboard/src/lib/clusters/types.ts b/dashboard/src/lib/clusters/types.ts index b650035f43..281f23787f 100644 --- a/dashboard/src/lib/clusters/types.ts +++ b/dashboard/src/lib/clusters/types.ts @@ -241,6 +241,7 @@ export type ClientMachineType = { type PreflightCheckResolutionStep = { text: string; externalLink?: string; + code?: string; }; export type PreflightCheckResolution = { title: string; @@ -508,6 +509,7 @@ const preflightCheckKeyValidator = z.enum([ "apiEnabled", "cidrAvailability", "iamPermissions", + "authz", ]); type PreflightCheckKey = z.infer; export const preflightCheckValidator = z.object({ @@ -519,7 +521,7 @@ export const preflightCheckValidator = z.object({ metadata: z.record(z.string()).optional(), }), }) - .array(), + .array() }); export const createContractResponseValidator = z.object({ contract_revision: z.object({ diff --git a/dashboard/src/lib/hooks/useCluster.ts b/dashboard/src/lib/hooks/useCluster.ts index 0d05f212a9..10e65ade07 100644 --- a/dashboard/src/lib/hooks/useCluster.ts +++ b/dashboard/src/lib/hooks/useCluster.ts @@ -10,9 +10,9 @@ import { updateExistingClusterContract, } from "lib/clusters"; import { - CloudProviderAWS, - CloudProviderGCP, - SUPPORTED_CLOUD_PROVIDERS, + CloudProviderAWS, CloudProviderAzure, + CloudProviderGCP, + SUPPORTED_CLOUD_PROVIDERS, } from "lib/clusters/constants"; import { clusterStateValidator, @@ -382,6 +382,7 @@ export const useUpdateCluster = ({ ) .with("AWS", () => CloudProviderAWS.preflightChecks) .with("GCP", () => CloudProviderGCP.preflightChecks) + .with("Azure", () => CloudProviderAzure.preflightChecks) .otherwise(() => []); const clientPreflightChecks: ClientPreflightCheck[] = parsed.errors diff --git a/dashboard/src/main/home/infrastructure-dashboard/modals/PreflightChecksModal.tsx b/dashboard/src/main/home/infrastructure-dashboard/modals/PreflightChecksModal.tsx index 0c76d8b2ee..6d47141e83 100644 --- a/dashboard/src/main/home/infrastructure-dashboard/modals/PreflightChecksModal.tsx +++ b/dashboard/src/main/home/infrastructure-dashboard/modals/PreflightChecksModal.tsx @@ -1,9 +1,10 @@ -import React, { useState } from "react"; +import React from "react"; import styled from "styled-components"; import { match } from "ts-pattern"; import Loading from "components/Loading"; import { Error as ErrorComponent } from "components/porter/Error"; +import Expandable from "components/porter/Expandable"; import Modal from "components/porter/Modal"; import Spacer from "components/porter/Spacer"; import StatusDot from "components/porter/StatusDot"; @@ -14,17 +15,15 @@ import ResolutionStepsModalContents from "./help/preflight/ResolutionStepsModalC type ItemProps = { preflightCheck: ClientPreflightCheck; + preExpanded?: boolean; }; -export const CheckItem: React.FC = ({ preflightCheck }) => { - const [isExpanded, setIsExpanded] = useState(true); - - return ( - - { - setIsExpanded(!isExpanded); - }} - > +export const CheckItem: React.FC = ({ + preflightCheck, + preExpanded = true, +}) => { + const renderHeader = (): React.ReactElement => { + return ( + {match(preflightCheck.status) .with("pending", () => ( @@ -37,42 +36,39 @@ export const CheckItem: React.FC = ({ preflightCheck }) => { )} {preflightCheck.title} - {preflightCheck.error && ( - - arrow_drop_down - + {preflightCheck?.error?.metadata?.quotaName && ( + {preflightCheck?.error?.metadata?.quotaName} )} - {isExpanded && preflightCheck.error && ( -
- - ) : undefined - } - /> - - {preflightCheck.error.metadata && - Object.entries(preflightCheck.error.metadata).map( - ([key, value]) => ( -
- {key}: - {value} -
- ) - )} -
- )} -
+ ); + }; + + if (!preflightCheck.error) { + return renderHeader(); + } + + return ( + +
+ + ) : undefined + } + /> + +
+
); }; @@ -90,13 +86,17 @@ const PreflightChecksModal: React.FC = ({ Cluster provision check - Your cloud provider account does not have enough resources to - provision this cluster. Please visit your cloud provider or change - your cluster configuration, then re-submit. + Your cloud provider account does not have the required permissions + and/or resources to provision with Porter. Please resolve the + following issues or change your cluster configuration and try again. - {preflightChecks.map((pfc) => ( - + {preflightChecks.map((pfc, idx) => ( + ))} @@ -124,43 +124,9 @@ const AppearingDiv = styled.div` } `; -const CheckItemContainer = styled.div` - display: flex; - flex-direction: column; - border: 1px solid ${(props) => props.theme.border}; - border-radius: 5px; - font-size: 13px; - width: 100%; - margin-bottom: 10px; - padding-left: 10px; - cursor: pointer; - background: ${(props) => props.theme.clickable.bg}; -`; - const CheckItemTop = styled.div` display: flex; align-items: center; padding: 10px; background: ${(props) => props.theme.clickable.bg}; `; - -const ExpandIcon = styled.i<{ isExpanded: boolean }>` - margin-left: 8px; - color: #ffffff66; - font-size: 20px; - cursor: pointer; - border-radius: 20px; - transform: ${(props) => (props.isExpanded ? "" : "rotate(-90deg)")}; -`; -const ErrorMessageLabel = styled.span` - font-weight: bold; - margin-left: 10px; -`; -const ErrorMessageContent = styled.div` - font-family: "Courier New", Courier, monospace; - padding: 5px 10px; - border-radius: 4px; - margin-left: 10px; - user-select: text; - cursor: text; -`; diff --git a/dashboard/src/main/home/infrastructure-dashboard/modals/help/preflight/ResolutionStepsModalContents.tsx b/dashboard/src/main/home/infrastructure-dashboard/modals/help/preflight/ResolutionStepsModalContents.tsx index 9cf1cbc2ba..870a8fed68 100644 --- a/dashboard/src/main/home/infrastructure-dashboard/modals/help/preflight/ResolutionStepsModalContents.tsx +++ b/dashboard/src/main/home/infrastructure-dashboard/modals/help/preflight/ResolutionStepsModalContents.tsx @@ -11,7 +11,7 @@ import { type PreflightCheckResolution } from "lib/clusters/types"; type Props = { resolution: PreflightCheckResolution; }; -const ElasticIPQuotaModalContents: React.FC = ({ resolution }) => { +const ResolutionStepsModalContents: React.FC = ({ resolution }) => { return (
@@ -41,7 +41,7 @@ const ElasticIPQuotaModalContents: React.FC = ({ resolution }) => { ); }; -export default ElasticIPQuotaModalContents; +export default ResolutionStepsModalContents; const StepContainer = styled.div` display: flex; diff --git a/go.mod b/go.mod index e12ec760db..01e366b518 100644 --- a/go.mod +++ b/go.mod @@ -83,7 +83,7 @@ require ( github.com/matryer/is v1.4.0 github.com/nats-io/nats.go v1.24.0 github.com/open-policy-agent/opa v0.44.0 - github.com/porter-dev/api-contracts v0.2.110 + github.com/porter-dev/api-contracts v0.2.111 github.com/riandyrn/otelchi v0.5.1 github.com/santhosh-tekuri/jsonschema/v5 v5.0.1 github.com/stefanmcshane/helm v0.0.0-20221213002717-88a4a2c6e77d diff --git a/go.sum b/go.sum index 8aee5d6997..ff6134a3b7 100644 --- a/go.sum +++ b/go.sum @@ -1523,8 +1523,8 @@ github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/polyfloyd/go-errorlint v0.0.0-20210722154253-910bb7978349/go.mod h1:wi9BfjxjF/bwiZ701TzmfKu6UKC357IOAtNr0Td0Lvw= -github.com/porter-dev/api-contracts v0.2.110 h1:/yfUCX4TtnynnqL4zUYMD+U96hkLDvgQqlrOuhZF7Ao= -github.com/porter-dev/api-contracts v0.2.110/go.mod h1:fX6JmP5QuzxDLvqP3evFOTXjI4dHxsG0+VKNTjImZU8= +github.com/porter-dev/api-contracts v0.2.111 h1:MMDIMumereUdKIK2yNZjhlCRgNz6jBh+uK+Kmf0qbTc= +github.com/porter-dev/api-contracts v0.2.111/go.mod h1:fX6JmP5QuzxDLvqP3evFOTXjI4dHxsG0+VKNTjImZU8= github.com/porter-dev/switchboard v0.0.3 h1:dBuYkiVLa5Ce7059d6qTe9a1C2XEORFEanhbtV92R+M= github.com/porter-dev/switchboard v0.0.3/go.mod h1:xSPzqSFMQ6OSbp42fhCi4AbGbQbsm6nRvOkrblFeXU4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= diff --git a/go.work.sum b/go.work.sum index 3c8d58fc49..c8cb94db20 100644 --- a/go.work.sum +++ b/go.work.sum @@ -856,6 +856,7 @@ github.com/porter-dev/api-contracts v0.2.73/go.mod h1:fX6JmP5QuzxDLvqP3evFOTXjI4 github.com/porter-dev/api-contracts v0.2.78 h1:Iyp1DL33mPxJZQSjH8W/ylv5Ch8i30eJJx9mvhZmhTU= github.com/porter-dev/api-contracts v0.2.78/go.mod h1:fX6JmP5QuzxDLvqP3evFOTXjI4dHxsG0+VKNTjImZU8= github.com/porter-dev/api-contracts v0.2.93/go.mod h1:fX6JmP5QuzxDLvqP3evFOTXjI4dHxsG0+VKNTjImZU8= +github.com/porter-dev/api-contracts v0.2.110/go.mod h1:fX6JmP5QuzxDLvqP3evFOTXjI4dHxsG0+VKNTjImZU8= github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= github.com/pquerna/cachecontrol v0.1.0 h1:yJMy84ti9h/+OEWa752kBTKv4XC30OtVVHYv/8cTqKc= github.com/pquerna/cachecontrol v0.1.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI=