From 62b2c2fcba195d745424450d65edaa39b3a1e55a Mon Sep 17 00:00:00 2001 From: Feroze Mohideen Date: Mon, 11 Sep 2023 15:14:41 -0400 Subject: [PATCH 01/59] remove revision from deploy event metadata (#3537) --- .../app-dashboard/app-view/tabs/activity-feed/ActivityFeed.tsx | 1 + .../app-dashboard/app-view/tabs/activity-feed/events/types.ts | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/ActivityFeed.tsx b/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/ActivityFeed.tsx index f5eef16e3f5..9eae2dd2b03 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/ActivityFeed.tsx +++ b/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/ActivityFeed.tsx @@ -57,6 +57,7 @@ const ActivityFeed: React.FC = ({ appName, deploymentTargetId, currentClu setHasError(false) } catch (err) { setHasError(true); + console.log(err); } finally { setLoading(false); setShouldAnimate(false); diff --git a/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/types.ts b/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/types.ts index 7fa335388a8..744f701b79c 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/types.ts +++ b/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/types.ts @@ -11,11 +11,10 @@ const porterAppAppEventMetadataValidator = z.object({ app_revision_id: z.string(), app_name: z.string(), app_id: z.string(), - agent_event_id: z.string(), + agent_event_id: z.number(), }); const porterAppDeployEventMetadataValidator = z.object({ image_tag: z.string(), - revision: z.number(), app_revision_id: z.string(), service_deployment_metadata: z.record(z.object({ status: z.string(), From d7f66fee527c421731508c709969759aa5565c9f Mon Sep 17 00:00:00 2001 From: sdess09 <37374498+sdess09@users.noreply.github.com> Date: Mon, 11 Sep 2023 16:23:38 -0400 Subject: [PATCH 02/59] Change Link to account settings (#3538) --- dashboard/src/components/CloudFormationForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dashboard/src/components/CloudFormationForm.tsx b/dashboard/src/components/CloudFormationForm.tsx index 06fba4c8a94..bb7dadc9c45 100644 --- a/dashboard/src/components/CloudFormationForm.tsx +++ b/dashboard/src/components/CloudFormationForm.tsx @@ -240,7 +240,7 @@ const CloudFormationForm: React.FC = ({ { - window.open("https://docs.aws.amazon.com/IAM/latest/UserGuide/FindingYourAWSId.html", "_blank") + window.open("https://us-east-1.console.aws.amazon.com/billing/home?region=us-east-1#/account", "_blank") }} > help_outline From fd721f2af9616f88e6b276b66066f24b055cc3c9 Mon Sep 17 00:00:00 2001 From: ianedwards Date: Mon, 11 Sep 2023 16:35:39 -0400 Subject: [PATCH 03/59] POR-1674 how banner if pr not merged or build not completed (#3540) Co-authored-by: Feroze Mohideen --- dashboard/src/lib/hooks/useGithubWorkflow.ts | 153 ++++++ .../app-view/AppDataContainer.tsx | 14 +- .../home/app-dashboard/app-view/AppView.tsx | 46 +- .../app-dashboard/app-view/RevisionsList.tsx | 502 ------------------ .../revisions-list/GHStatusBanner.tsx | 93 ++++ .../revisions-list/RevisionTableContents.tsx | 380 +++++++++++++ .../revisions-list/RevisionsList.tsx | 179 +++++++ 7 files changed, 814 insertions(+), 553 deletions(-) create mode 100644 dashboard/src/lib/hooks/useGithubWorkflow.ts delete mode 100644 dashboard/src/main/home/app-dashboard/app-view/RevisionsList.tsx create mode 100644 dashboard/src/main/home/app-dashboard/validate-apply/revisions-list/GHStatusBanner.tsx create mode 100644 dashboard/src/main/home/app-dashboard/validate-apply/revisions-list/RevisionTableContents.tsx create mode 100644 dashboard/src/main/home/app-dashboard/validate-apply/revisions-list/RevisionsList.tsx diff --git a/dashboard/src/lib/hooks/useGithubWorkflow.ts b/dashboard/src/lib/hooks/useGithubWorkflow.ts new file mode 100644 index 00000000000..25a44556093 --- /dev/null +++ b/dashboard/src/lib/hooks/useGithubWorkflow.ts @@ -0,0 +1,153 @@ +import { useQueries } from "@tanstack/react-query"; +import axios from "axios"; +import { PorterAppRecord } from "main/home/app-dashboard/app-view/AppView"; +import { useCallback, useContext, useEffect, useMemo, useState } from "react"; +import { Context } from "shared/Context"; +import api from "shared/api"; +import { z } from "zod"; + +export const useGithubWorkflow = ( + porterApp: PorterAppRecord, + previouslyBuilt: boolean +) => { + const { currentProject, currentCluster } = useContext(Context); + const [githubWorkflowFilename, setGithubWorkflowName] = useState(""); + const [userHasGithubAccess, setUserHasGithubAccess] = useState(true); + + const gitMetadata = useMemo(() => { + const repoNameParts = z + .tuple([z.string(), z.string()]) + .safeParse(porterApp.repo_name?.split("/")); + if ( + !repoNameParts.success || + !porterApp.git_repo_id || + !porterApp.git_branch + ) { + return { + repo_id: 0, + owner: "", + name: "", + branch: "", + }; + } + + return { + repo_id: porterApp.git_repo_id, + owner: repoNameParts.data[0], + name: repoNameParts.data[1], + branch: porterApp.git_branch, + }; + }, [porterApp.git_repo_id, porterApp.repo_name, porterApp.git_branch]); + + const fetchGithubWorkflow = useCallback( + async (fileName: string) => { + try { + if (githubWorkflowFilename !== "") { + return githubWorkflowFilename; + } + + if (currentProject == null || currentCluster == null) { + return ""; + } + + const res = await api.getBranchContents( + "", + { + dir: `./.github/workflows/${fileName}`, + }, + { + project_id: currentProject.id, + git_repo_id: gitMetadata.repo_id, + kind: "github", + owner: gitMetadata.owner, + name: gitMetadata.name, + branch: gitMetadata.branch, + } + ); + + if (res.data) { + return fileName; + } + + return ""; + } catch (err) { + return ""; + } + }, + [currentProject, currentCluster, gitMetadata, githubWorkflowFilename] + ); + + const enabled = + !previouslyBuilt && + !!currentProject && + !!currentCluster && + githubWorkflowFilename === ""; + + const [ + { + data: applicationWorkflowCheck, + isLoading: isLoadingApplicationWorkflow, + }, + { data: defaultWorkflowCheck, isLoading: isLoadingDefaultWorkflow }, + ] = useQueries({ + queries: [ + { + queryKey: [ + `checkForApplicationWorkflow_porter_stack_${porterApp.name}`, + currentProject?.id, + currentCluster?.id, + githubWorkflowFilename, + previouslyBuilt, + ], + queryFn: () => + fetchGithubWorkflow(`porter_stack_${porterApp.name}.yml`), + enabled, + refetchInterval: 5000, + retry: (_failureCount: number, error: unknown) => { + if (axios.isAxiosError(error) && error.response?.status === 403) { + setUserHasGithubAccess(false); + return false; + } + + return true; + }, + refetchOnWindowFocus: false, + }, + { + queryKey: [ + `checkForApplicationWorkflow_porter`, + currentProject?.id, + currentCluster?.id, + githubWorkflowFilename, + previouslyBuilt, + ], + queryFn: () => fetchGithubWorkflow("porter.yml"), + enabled, + refetchInterval: 5000, + retry: (_failureCount: number, error: unknown) => { + if (axios.isAxiosError(error) && error.response?.status === 403) { + setUserHasGithubAccess(false); + return false; + } + + return true; + }, + refetchOnWindowFocus: false, + }, + ], + }); + + useEffect(() => { + if (!!applicationWorkflowCheck) { + setGithubWorkflowName(applicationWorkflowCheck); + } else if (!!defaultWorkflowCheck) { + setGithubWorkflowName(defaultWorkflowCheck); + } + }, [applicationWorkflowCheck, defaultWorkflowCheck]); + + return { + githubWorkflowFilename, + isLoading: isLoadingApplicationWorkflow || isLoadingDefaultWorkflow, + userHasGithubAccess, + }; +}; diff --git a/dashboard/src/main/home/app-dashboard/app-view/AppDataContainer.tsx b/dashboard/src/main/home/app-dashboard/app-view/AppDataContainer.tsx index 8c5794d1054..2594452c99e 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/AppDataContainer.tsx +++ b/dashboard/src/main/home/app-dashboard/app-view/AppDataContainer.tsx @@ -7,7 +7,6 @@ import { porterAppFormValidator, } from "lib/porter-apps"; import { zodResolver } from "@hookform/resolvers/zod"; -import RevisionsList from "./RevisionsList"; import { useLatestRevision } from "./LatestRevisionContext"; import Spacer from "components/porter/Spacer"; import TabSelector from "components/TabSelector"; @@ -27,6 +26,7 @@ import Icon from "components/porter/Icon"; import save from "assets/save-01.svg"; import LogsTab from "./tabs/LogsTab"; import MetricsTab from "./tabs/MetricsTab"; +import RevisionsList from "../validate-apply/revisions-list/RevisionsList"; import Activity from "./tabs/Activity"; // commented out tabs are not yet implemented @@ -197,7 +197,7 @@ const AppDataContainer: React.FC = ({ tabParam }) => { porterApp.name, ]); setPreviewRevision(null); - } catch (err) { } + } catch (err) {} }); useEffect(() => { @@ -258,11 +258,11 @@ const AppDataContainer: React.FC = ({ tabParam }) => { { label: "Environment", value: "environment" }, ...(latestProto.build ? [ - { - label: "Build Settings", - value: "build-settings", - }, - ] + { + label: "Build Settings", + value: "build-settings", + }, + ] : []), { label: "Settings", value: "settings" }, ]} diff --git a/dashboard/src/main/home/app-dashboard/app-view/AppView.tsx b/dashboard/src/main/home/app-dashboard/app-view/AppView.tsx index 47f5e25b55c..828fe1e1fd4 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/AppView.tsx +++ b/dashboard/src/main/home/app-dashboard/app-view/AppView.tsx @@ -23,6 +23,7 @@ export const porterAppValidator = z.object({ dockerfile: z.string().optional(), image_repo_uri: z.string().optional(), porter_yaml_path: z.string().optional(), + pull_request_url: z.string().optional(), }); export type PorterAppRecord = z.infer; @@ -102,47 +103,4 @@ const StyledExpandedApp = styled.div` opacity: 1; } } -`; -const A = styled.a` - display: flex; - align-items: center; -`; -const SmallIcon = styled.img<{ opacity?: string; height?: string }>` - height: ${(props) => props.height || "15px"}; - opacity: ${(props) => props.opacity || 1}; - margin-right: 10px; -`; -const BranchIcon = styled.img` - height: 14px; - opacity: 0.65; - margin-right: 5px; -`; -const TagWrapper = styled.div` - height: 20px; - font-size: 12px; - display: flex; - align-items: center; - justify-content: center; - color: #ffffff44; - border: 1px solid #ffffff44; - border-radius: 3px; - padding-left: 6px; -`; -const BranchTag = styled.div` - height: 20px; - margin-left: 6px; - color: #aaaabb; - background: #ffffff22; - border-radius: 3px; - font-size: 12px; - display: flex; - align-items: center; - justify-content: center; - padding: 0px 6px; - padding-left: 7px; - border-top-left-radius: 0px; - border-bottom-left-radius: 0px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -`; +`; \ No newline at end of file diff --git a/dashboard/src/main/home/app-dashboard/app-view/RevisionsList.tsx b/dashboard/src/main/home/app-dashboard/app-view/RevisionsList.tsx deleted file mode 100644 index f8c36c0a70e..00000000000 --- a/dashboard/src/main/home/app-dashboard/app-view/RevisionsList.tsx +++ /dev/null @@ -1,502 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { AppRevision, appRevisionValidator } from "lib/revisions/types"; -import React, { useCallback, useState } from "react"; -import api from "shared/api"; -import styled from "styled-components"; -import { match } from "ts-pattern"; -import loading from "assets/loading.gif"; -import { - PorterAppFormData, - SourceOptions, - clientAppFromProto, -} from "lib/porter-apps"; -import { z } from "zod"; -import { PorterApp } from "@porter-dev/api-contracts"; -import { readableDate } from "shared/string_utils"; -import Text from "components/porter/Text"; -import { useLatestRevision } from "./LatestRevisionContext"; -import { useFormContext } from "react-hook-form"; -import ConfirmOverlay from "components/porter/ConfirmOverlay"; - -type Props = { - deploymentTargetId: string; - projectId: number; - clusterId: number; - appName: string; - latestSource: SourceOptions; - latestRevisionNumber: number; - onSubmit: () => Promise; -}; - -const RED = "#ff0000"; -const YELLOW = "#FFA500"; - -const RevisionsList: React.FC = ({ - latestRevisionNumber, - deploymentTargetId, - projectId, - clusterId, - appName, - latestSource, - onSubmit, -}) => { - const { - previewRevision, - setPreviewRevision, - servicesFromYaml, - } = useLatestRevision(); - const { reset, setValue } = useFormContext(); - const [expandRevisions, setExpandRevisions] = useState(false); - const [revertData, setRevertData] = useState<{ - app: PorterApp; - revision: number; - } | null>(null); - - const res = useQuery( - ["listAppRevisions", projectId, clusterId, latestRevisionNumber, appName], - async () => { - const res = await api.listAppRevisions( - "", - { - deployment_target_id: deploymentTargetId, - }, - { - project_id: projectId, - cluster_id: clusterId, - porter_app_name: appName, - } - ); - - const revisions = await z - .object({ - app_revisions: z.array(appRevisionValidator), - }) - .parseAsync(res.data); - - return revisions; - } - ); - - const getReadableStatus = (status: AppRevision["status"]) => - match(status) - .with("CREATED", () => "Created") - .with("AWAITING_BUILD_ARTIFACT", () => "Awaiting Build") - .with("READY_TO_APPLY", () => "Deploying") - .with("AWAITING_PREDEPLOY", () => "Awaiting Pre-Deploy") - .with("BUILD_CANCELED", () => "Build Canceled") - .with("BUILD_FAILED", () => "Build Failed") - .with("DEPLOY_FAILED", () => "Deploy Failed") - .with("DEPLOYED", () => "Deployed") - .exhaustive(); - - const getDotColor = (status: AppRevision["status"]) => - match(status) - .with( - "CREATED", - "AWAITING_BUILD_ARTIFACT", - "READY_TO_APPLY", - "AWAITING_PREDEPLOY", - () => YELLOW - ) - .otherwise(() => RED); - - const getTableHeader = (latestRevision?: AppRevision) => { - if (!latestRevision) { - return "Revisions"; - } - - if (previewRevision) { - return "Previewing revision (not deployed) -"; - } - - return "Current revision - "; - }; - - const getSelectedRevisionNumber = (args: { - numDeployed: number; - latestRevision?: AppRevision; - }) => { - const { numDeployed, latestRevision } = args; - - if (previewRevision) { - return previewRevision; - } - - if (latestRevision && latestRevision.revision_number !== 0) { - return latestRevision.revision_number; - } - - return numDeployed + 1; - }; - - const onRevert = useCallback(async () => { - if (!revertData) { - return; - } - - setValue("app", clientAppFromProto(revertData.app, servicesFromYaml)); - setRevertData(null); - - void onSubmit(); - }, [onSubmit, setValue, revertData]); - - const renderContents = (revisions: AppRevision[]) => { - const revisionsWithProto = revisions.map((revision) => { - return { - ...revision, - app_proto: PorterApp.fromJsonString(atob(revision.b64_app_proto)), - }; - }); - - const deployedRevisions = revisionsWithProto.filter( - (r) => r.revision_number !== 0 - ); - const pendingRevisions = revisionsWithProto.filter( - (r) => r.revision_number === 0 - ); - - return ( -
- { - setExpandRevisions((prev) => !prev); - }} - > - - arrow_drop_down - {getTableHeader(revisions[0])} - {revisions[0] ? ( - - No.{" "} - {getSelectedRevisionNumber({ - numDeployed: deployedRevisions.length, - latestRevision: revisions[0], - })} - - ) : null} - - - - - - - - Revision no. - - {revisionsWithProto[0]?.app_proto.build - ? "Commit SHA" - : "Image Tag"} - - Timestamp - Status - Rollback - - {pendingRevisions.length > 0 && - pendingRevisions.map((revision) => ( - - {deployedRevisions.length + 1} - - {revision.app_proto.build - ? revision.app_proto.build.commitSha.substring(0, 7) - : revision.app_proto.image?.tag} - - {readableDate(revision.updated_at)} - - - {getReadableStatus(revision.status)} - - - - - - - ))} - - {deployedRevisions.map((revision, i) => { - const isLatestDeployedRevision = - latestRevisionNumber !== 0 - ? revision.revision_number === latestRevisionNumber - : i === 0; - - return ( - { - reset({ - app: clientAppFromProto( - revision.app_proto, - servicesFromYaml - ), - source: latestSource, - }); - setPreviewRevision( - isLatestDeployedRevision - ? null - : revision.revision_number - ); - }} - > - {revision.revision_number} - - - {revision.app_proto.build - ? revision.app_proto.build.commitSha.substring(0, 7) - : revision.app_proto.image?.tag} - - {readableDate(revision.updated_at)} - - {!isLatestDeployedRevision ? ( - getReadableStatus(revision.status) - ) : ( - - {getReadableStatus(revision.status)} - - - )} - - - { - if (isLatestDeployedRevision) { - return; - } - - setRevertData({ - app: revision.app_proto, - revision: revision.revision_number, - }); - }} - > - {isLatestDeployedRevision ? "Current" : "Revert"} - - - - ); - })} - - - - -
- ); - }; - - return ( - - {match(res) - .with({ status: "loading" }, () => ( - - - Updating . . . - - - )) - .with({ status: "success" }, ({ data }) => - renderContents(data.app_revisions) - ) - .otherwise(() => null)} - {revertData ? ( - { - setRevertData(null); - }} - /> - ) : null} - - ); -}; - -export default RevisionsList; - -const StyledRevisionSection = styled.div` - width: 100%; - max-height: ${(props: { showRevisions: boolean }) => - props.showRevisions ? "255px" : "40px"}; - margin: 20px 0px 18px; - overflow: hidden; - border-radius: 5px; - background: ${(props) => props.theme.fg}; - border: 1px solid #494b4f; - :hover { - border: 1px solid #7a7b80; - } - animation: ${(props: { showRevisions: boolean }) => - props.showRevisions ? "expandRevisions 0.3s" : ""}; - animation-timing-function: ease-out; - @keyframes expandRevisions { - from { - max-height: 40px; - } - to { - max-height: 250px; - } - } -`; - -const LoadingPlaceholder = styled.div` - height: 40px; - display: flex; - align-items: center; - padding-left: 20px; -`; - -const LoadingGif = styled.img` - width: 15px; - height: 15px; - margin-right: ${(props: { revision: boolean }) => - props.revision ? "0px" : "9px"}; - margin-left: ${(props: { revision: boolean }) => - props.revision ? "10px" : "0px"}; - margin-bottom: ${(props: { revision: boolean }) => - props.revision ? "-2px" : "0px"}; -`; - -const StatusWrapper = styled.div` - display: flex; - align-items: center; - font-family: "Work Sans", sans-serif; - font-size: 13px; - color: #ffffff55; - margin-right: 25px; -`; - -const RevisionHeader = styled.div` - color: ${(props: { showRevisions: boolean; isCurrent: boolean }) => - props.isCurrent ? "#ffffff66" : "#f5cb42"}; - display: flex; - justify-content: space-between; - align-items: center; - height: 40px; - font-size: 13px; - width: 100%; - padding-left: 10px; - cursor: pointer; - background: ${({ theme }) => theme.fg}; - :hover { - background: ${(props) => props.showRevisions && props.theme.fg2}; - } - > div > i { - margin-right: 8px; - font-size: 20px; - cursor: pointer; - border-radius: 20px; - transform: ${(props: { showRevisions: boolean; isCurrent: boolean }) => - props.showRevisions ? "" : "rotate(-90deg)"}; - transition: transform 0.1s ease; - } -`; - -const RevisionPreview = styled.div` - display: flex; - align-items: center; -`; - -const Revision = styled.div` - color: #ffffff; - margin-left: 5px; -`; - -const RevisionList = styled.div` - overflow-y: auto; - max-height: 215px; -`; - -const TableWrapper = styled.div` - padding-bottom: 20px; -`; - -const RevisionsTable = styled.table` - width: 100%; - margin-top: 5px; - padding-left: 32px; - padding-bottom: 20px; - min-width: 500px; - border-collapse: collapse; -`; - -const Tr = styled.tr` - line-height: 2.2em; - cursor: ${(props: { disableHover?: boolean; selected?: boolean }) => - props.disableHover ? "" : "pointer"}; - background: ${(props: { disableHover?: boolean; selected?: boolean }) => - props.selected ? "#ffffff11" : ""}; - :hover { - background: ${(props: { disableHover?: boolean; selected?: boolean }) => - props.disableHover ? "" : "#ffffff22"}; - } -`; - -const Td = styled.td` - font-size: 13px; - color: #ffffff; - padding-left: 32px; -`; - -const Th = styled.td` - font-size: 13px; - font-weight: 500; - color: #aaaabb; - padding-left: 32px; -`; - -const RollbackButton = styled.div` - cursor: ${(props: { disabled: boolean }) => - props.disabled ? "not-allowed" : "pointer"}; - display: flex; - border-radius: 3px; - align-items: center; - justify-content: center; - font-weight: 500; - height: 21px; - font-size: 13px; - width: 70px; - background: ${(props: { disabled: boolean }) => - props.disabled ? "#aaaabbee" : "#616FEEcc"}; - :hover { - background: ${(props: { disabled: boolean }) => - props.disabled ? "" : "#405eddbb"}; - } -`; - -const StatusContainer = styled.div` - display: flex; - align-items: center; -`; - -const StatusDot = styled.div<{ color?: string }>` - min-width: 7px; - max-width: 7px; - height: 7px; - margin-left: 10px; - border-radius: 50%; - background: ${(props) => props.color || "#38a88a"}; - - box-shadow: 0 0 0 0 rgba(0, 0, 0, 1); - transform: scale(1); - animation: pulse 2s infinite; - @keyframes pulse { - 0% { - transform: scale(0.95); - box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.9); - } - - 70% { - transform: scale(1); - box-shadow: 0 0 0 10px rgba(0, 0, 0, 0); - } - - 100% { - transform: scale(0.95); - box-shadow: 0 0 0 0 rgba(0, 0, 0, 0); - } - } -`; diff --git a/dashboard/src/main/home/app-dashboard/validate-apply/revisions-list/GHStatusBanner.tsx b/dashboard/src/main/home/app-dashboard/validate-apply/revisions-list/GHStatusBanner.tsx new file mode 100644 index 00000000000..07009f4ebda --- /dev/null +++ b/dashboard/src/main/home/app-dashboard/validate-apply/revisions-list/GHStatusBanner.tsx @@ -0,0 +1,93 @@ +import React, { useContext, useMemo } from "react"; +import { useLatestRevision } from "../../app-view/LatestRevisionContext"; +import { Context } from "shared/Context"; +import { useGithubWorkflow } from "lib/hooks/useGithubWorkflow"; +import Banner from "components/porter/Banner"; +import Spacer from "components/porter/Spacer"; +import Link from "components/porter/Link"; +import GHABanner from "../../expanded-app/GHABanner"; +import { match } from "ts-pattern"; +import { AppRevision } from "lib/revisions/types"; + +type GHStatusBannerProps = { + revisions: AppRevision[]; +}; + +const GHStatusBanner: React.FC = ({ revisions }) => { + const { setCurrentModal } = useContext(Context); + const { porterApp } = useLatestRevision(); + + const previouslyBuilt = useMemo(() => { + return revisions.some((r) => + match(r.status) + .with( + "AWAITING_PREDEPLOY", + "READY_TO_APPLY", + "DEPLOYED", + "DEPLOY_FAILED", + "BUILD_FAILED", + () => true + ) + .otherwise(() => false) + ); + }, [revisions]); + + const { + githubWorkflowFilename, + userHasGithubAccess, + isLoading, + } = useGithubWorkflow(porterApp, previouslyBuilt); + + if (previouslyBuilt) { + return null; + } + + if (!userHasGithubAccess) { + return ( + + You do not have access to the GitHub repo associated with this + application. + + setCurrentModal?.("AccountSettingsModal", {})} + > + Check account settings + + + ); + } + + if (githubWorkflowFilename) { + return ( + + Your GitHub repo has not been built yet. + + + Check status + + + ); + } + + if (!isLoading) { + return ( + + ); + } + + return null +}; + +export default GHStatusBanner; diff --git a/dashboard/src/main/home/app-dashboard/validate-apply/revisions-list/RevisionTableContents.tsx b/dashboard/src/main/home/app-dashboard/validate-apply/revisions-list/RevisionTableContents.tsx new file mode 100644 index 00000000000..eaa5d8abc94 --- /dev/null +++ b/dashboard/src/main/home/app-dashboard/validate-apply/revisions-list/RevisionTableContents.tsx @@ -0,0 +1,380 @@ +import React, { Dispatch, SetStateAction, useMemo } from "react"; +import { PorterApp } from "@porter-dev/api-contracts"; +import { AppRevision } from "lib/revisions/types"; +import { match } from "ts-pattern"; +import { useLatestRevision } from "../../app-view/LatestRevisionContext"; +import styled from "styled-components"; +import { readableDate } from "shared/string_utils"; +import Text from "components/porter/Text"; +import { useGithubWorkflow } from "lib/hooks/useGithubWorkflow"; +import { useFormContext } from "react-hook-form"; +import { + PorterAppFormData, + SourceOptions, + clientAppFromProto, +} from "lib/porter-apps"; +import GHStatusBanner from "./GHStatusBanner"; + +type RevisionTableContentsProps = { + latestRevisionNumber: number; + revisions: AppRevision[]; + latestSource: SourceOptions; + expandRevisions: boolean; + setExpandRevisions: Dispatch>; + setRevertData: Dispatch< + SetStateAction<{ + app: PorterApp; + revision: number; + } | null> + >; +}; + +const RED = "#ff0000"; +const YELLOW = "#FFA500"; + +const RevisionTableContents: React.FC = ({ + latestRevisionNumber, + revisions, + latestSource, + expandRevisions, + setExpandRevisions, + setRevertData, +}) => { + const { reset } = useFormContext(); + const { + previewRevision, + setPreviewRevision, + servicesFromYaml, + } = useLatestRevision(); + + const revisionsWithProto = revisions.map((revision) => { + return { + ...revision, + app_proto: PorterApp.fromJsonString(atob(revision.b64_app_proto)), + }; + }); + + const deployedRevisions = revisionsWithProto.filter( + (r) => r.revision_number !== 0 + ); + const pendingRevisions = revisionsWithProto.filter( + (r) => r.revision_number === 0 + ); + const getReadableStatus = (status: AppRevision["status"]) => + match(status) + .with("CREATED", () => "Created") + .with("AWAITING_BUILD_ARTIFACT", () => "Awaiting Build") + .with("READY_TO_APPLY", () => "Deploying") + .with("AWAITING_PREDEPLOY", () => "Awaiting Pre-Deploy") + .with("BUILD_CANCELED", () => "Build Canceled") + .with("BUILD_FAILED", () => "Build Failed") + .with("DEPLOY_FAILED", () => "Deploy Failed") + .with("DEPLOYED", () => "Deployed") + .exhaustive(); + + const getDotColor = (status: AppRevision["status"]) => + match(status) + .with( + "CREATED", + "AWAITING_BUILD_ARTIFACT", + "READY_TO_APPLY", + "AWAITING_PREDEPLOY", + () => YELLOW + ) + .otherwise(() => RED); + + const getTableHeader = (latestRevision?: AppRevision) => { + if (!latestRevision) { + return "Revisions"; + } + + if (previewRevision) { + return "Previewing revision (not deployed) -"; + } + + return "Current revision - "; + }; + + const getSelectedRevisionNumber = (args: { + numDeployed: number; + latestRevision?: AppRevision; + }) => { + const { numDeployed, latestRevision } = args; + + if (previewRevision) { + return previewRevision; + } + + if (latestRevision && latestRevision.revision_number !== 0) { + return latestRevision.revision_number; + } + + return numDeployed + 1; + }; + + return ( +
+ { + setExpandRevisions((prev) => !prev); + }} + > + + arrow_drop_down + {getTableHeader(revisions[0])} + {revisions[0] ? ( + + No.{" "} + {getSelectedRevisionNumber({ + numDeployed: deployedRevisions.length, + latestRevision: revisions[0], + })} + + ) : null} + + + + + + + + Revision no. + + {revisionsWithProto[0]?.app_proto.build + ? "Commit SHA" + : "Image Tag"} + + Timestamp + Status + Rollback + + {pendingRevisions.length > 0 && + pendingRevisions.map((revision) => ( + + {deployedRevisions.length + 1} + + {revision.app_proto.build + ? revision.app_proto.build.commitSha.substring(0, 7) + : revision.app_proto.image?.tag} + + {readableDate(revision.updated_at)} + + + {getReadableStatus(revision.status)} + + + + - + + ))} + + {deployedRevisions.map((revision, i) => { + const isLatestDeployedRevision = + latestRevisionNumber !== 0 + ? revision.revision_number === latestRevisionNumber + : i === 0; + + return ( + { + reset({ + app: clientAppFromProto( + revision.app_proto, + servicesFromYaml + ), + source: latestSource, + }); + setPreviewRevision( + isLatestDeployedRevision + ? null + : revision.revision_number + ); + }} + > + {revision.revision_number} + + + {revision.app_proto.build + ? revision.app_proto.build.commitSha.substring(0, 7) + : revision.app_proto.image?.tag} + + {readableDate(revision.updated_at)} + + {!isLatestDeployedRevision ? ( + getReadableStatus(revision.status) + ) : ( + + {getReadableStatus(revision.status)} + + + )} + + + { + if (isLatestDeployedRevision) { + return; + } + + setRevertData({ + app: revision.app_proto, + revision: revision.revision_number, + }); + }} + > + {isLatestDeployedRevision ? "Current" : "Revert"} + + + + ); + })} + + + + +
+ ); +}; + +export default RevisionTableContents; + +const RevisionHeader = styled.div` + color: ${(props: { showRevisions: boolean; isCurrent: boolean }) => + props.isCurrent ? "#ffffff66" : "#f5cb42"}; + display: flex; + justify-content: space-between; + align-items: center; + height: 40px; + font-size: 13px; + width: 100%; + padding-left: 10px; + cursor: pointer; + background: ${({ theme }) => theme.fg}; + :hover { + background: ${(props) => props.showRevisions && props.theme.fg2}; + } + > div > i { + margin-right: 8px; + font-size: 20px; + cursor: pointer; + border-radius: 20px; + transform: ${(props: { showRevisions: boolean; isCurrent: boolean }) => + props.showRevisions ? "" : "rotate(-90deg)"}; + transition: transform 0.1s ease; + } +`; + +const RevisionPreview = styled.div` + display: flex; + align-items: center; +`; + +const Revision = styled.div` + color: #ffffff; + margin-left: 5px; +`; + +const RevisionList = styled.div` + overflow-y: auto; + max-height: 215px; +`; + +const TableWrapper = styled.div` + padding-bottom: 20px; +`; + +const RevisionsTable = styled.table` + width: 100%; + margin-top: 5px; + padding-left: 32px; + padding-bottom: 20px; + min-width: 500px; + border-collapse: collapse; +`; + +const Tr = styled.tr` + line-height: 2.2em; + cursor: ${(props: { disableHover?: boolean; selected?: boolean }) => + props.disableHover ? "" : "pointer"}; + background: ${(props: { disableHover?: boolean; selected?: boolean }) => + props.selected ? "#ffffff11" : ""}; + :hover { + background: ${(props: { disableHover?: boolean; selected?: boolean }) => + props.disableHover ? "" : "#ffffff22"}; + } +`; + +const Td = styled.td` + font-size: 13px; + color: #ffffff; + padding-left: 32px; +`; + +const Th = styled.td` + font-size: 13px; + font-weight: 500; + color: #aaaabb; + padding-left: 32px; +`; + +const RollbackButton = styled.div` + cursor: ${(props: { disabled: boolean }) => + props.disabled ? "not-allowed" : "pointer"}; + display: flex; + border-radius: 3px; + align-items: center; + justify-content: center; + font-weight: 500; + height: 21px; + font-size: 13px; + width: 70px; + background: ${(props: { disabled: boolean }) => + props.disabled ? "#aaaabbee" : "#616FEEcc"}; + :hover { + background: ${(props: { disabled: boolean }) => + props.disabled ? "" : "#405eddbb"}; + } +`; + +const StatusContainer = styled.div` + display: flex; + align-items: center; +`; + +const StatusDot = styled.div<{ color?: string }>` + min-width: 7px; + max-width: 7px; + height: 7px; + margin-left: 10px; + border-radius: 50%; + background: ${(props) => props.color || "#38a88a"}; + + box-shadow: 0 0 0 0 rgba(0, 0, 0, 1); + transform: scale(1); + animation: pulse 2s infinite; + @keyframes pulse { + 0% { + transform: scale(0.95); + box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.9); + } + + 70% { + transform: scale(1); + box-shadow: 0 0 0 10px rgba(0, 0, 0, 0); + } + + 100% { + transform: scale(0.95); + box-shadow: 0 0 0 0 rgba(0, 0, 0, 0); + } + } +`; diff --git a/dashboard/src/main/home/app-dashboard/validate-apply/revisions-list/RevisionsList.tsx b/dashboard/src/main/home/app-dashboard/validate-apply/revisions-list/RevisionsList.tsx new file mode 100644 index 00000000000..c99a627a609 --- /dev/null +++ b/dashboard/src/main/home/app-dashboard/validate-apply/revisions-list/RevisionsList.tsx @@ -0,0 +1,179 @@ +import { useQuery } from "@tanstack/react-query"; +import { appRevisionValidator } from "lib/revisions/types"; +import React, { useCallback, useState } from "react"; +import api from "shared/api"; +import styled from "styled-components"; +import { match } from "ts-pattern"; +import loading from "assets/loading.gif"; +import { + PorterAppFormData, + SourceOptions, + clientAppFromProto, +} from "lib/porter-apps"; +import { z } from "zod"; +import { PorterApp } from "@porter-dev/api-contracts"; +import { useFormContext } from "react-hook-form"; +import ConfirmOverlay from "components/porter/ConfirmOverlay"; +import { useLatestRevision } from "../../app-view/LatestRevisionContext"; +import RevisionTableContents from "./RevisionTableContents"; +import GHStatusBanner from "./GHStatusBanner"; +import Spacer from "components/porter/Spacer"; + +type Props = { + deploymentTargetId: string; + projectId: number; + clusterId: number; + appName: string; + latestSource: SourceOptions; + latestRevisionNumber: number; + onSubmit: () => Promise; +}; + +const RevisionsList: React.FC = ({ + latestRevisionNumber, + deploymentTargetId, + projectId, + clusterId, + appName, + latestSource, + onSubmit, +}) => { + const { servicesFromYaml } = useLatestRevision(); + const { setValue } = useFormContext(); + const [expandRevisions, setExpandRevisions] = useState(false); + const [revertData, setRevertData] = useState<{ + app: PorterApp; + revision: number; + } | null>(null); + + const res = useQuery( + ["listAppRevisions", projectId, clusterId, latestRevisionNumber, appName], + async () => { + const res = await api.listAppRevisions( + "", + { + deployment_target_id: deploymentTargetId, + }, + { + project_id: projectId, + cluster_id: clusterId, + porter_app_name: appName, + } + ); + + const revisions = await z + .object({ + app_revisions: z.array(appRevisionValidator), + }) + .parseAsync(res.data); + + return revisions; + } + ); + + const onRevert = useCallback(async () => { + if (!revertData) { + return; + } + + setValue("app", clientAppFromProto(revertData.app, servicesFromYaml)); + setRevertData(null); + + void onSubmit(); + }, [onSubmit, setValue, revertData]); + + return ( +
+ + {match(res) + .with({ status: "loading" }, () => ( + + + Updating . . . + + + )) + .with({ status: "success" }, ({ data }) => ( + + )) + .otherwise(() => null)} + {revertData ? ( + { + setRevertData(null); + }} + /> + ) : null} + + {res.data && ( + <> + + + + )} +
+ ); +}; + +export default RevisionsList; + +const StyledRevisionSection = styled.div` + width: 100%; + max-height: ${(props: { showRevisions: boolean }) => + props.showRevisions ? "255px" : "40px"}; + margin: 20px 0px 18px; + overflow: hidden; + border-radius: 5px; + background: ${(props) => props.theme.fg}; + border: 1px solid #494b4f; + :hover { + border: 1px solid #7a7b80; + } + animation: ${(props: { showRevisions: boolean }) => + props.showRevisions ? "expandRevisions 0.3s" : ""}; + animation-timing-function: ease-out; + @keyframes expandRevisions { + from { + max-height: 40px; + } + to { + max-height: 250px; + } + } +`; + +const LoadingPlaceholder = styled.div` + height: 40px; + display: flex; + align-items: center; + padding-left: 20px; +`; + +const LoadingGif = styled.img` + width: 15px; + height: 15px; + margin-right: ${(props: { revision: boolean }) => + props.revision ? "0px" : "9px"}; + margin-left: ${(props: { revision: boolean }) => + props.revision ? "10px" : "0px"}; + margin-bottom: ${(props: { revision: boolean }) => + props.revision ? "-2px" : "0px"}; +`; + +const StatusWrapper = styled.div` + display: flex; + align-items: center; + font-family: "Work Sans", sans-serif; + font-size: 13px; + color: #ffffff55; + margin-right: 25px; +`; From 7fda8575d911a31b9751481030347fe7cd4c1f11 Mon Sep 17 00:00:00 2001 From: jose-fully-ported <141160579+jose-fully-ported@users.noreply.github.com> Date: Mon, 11 Sep 2023 16:43:50 -0400 Subject: [PATCH 04/59] fix: respect launchdarkly responses for feature flags when doing a full page refresh (#3541) --- dashboard/src/main/home/Home.tsx | 34 +++++++------------------------- 1 file changed, 7 insertions(+), 27 deletions(-) diff --git a/dashboard/src/main/home/Home.tsx b/dashboard/src/main/home/Home.tsx index c3b0b153260..be625161b44 100644 --- a/dashboard/src/main/home/Home.tsx +++ b/dashboard/src/main/home/Home.tsx @@ -134,35 +134,15 @@ const Home: React.FC = (props) => { } else if (projectList.length > 0 && !currentProject) { setProjects(projectList); - let foundProject = null; - if (id) { - projectList.forEach((project: ProjectListType, i: number) => { - if (project.id === id) { - foundProject = project; - } - }); - - const project = await api - .getProject("", {}, { id: projectList[0].id }) - .then((res) => res.data as ProjectType); - - setCurrentProject(foundProject || project); + if (!id) { + id = Number(localStorage.getItem("currentProject")) || projectList[0].id } - if (!foundProject) { - projectList.forEach((project: ProjectListType, i: number) => { - if ( - project.id.toString() === - localStorage.getItem("currentProject") - ) { - foundProject = project; - } - }); - const project = await api - .getProject("", {}, { id: projectList[0].id }) - .then((res) => res.data as ProjectType); - setCurrentProject(foundProject || project); - } + const project = await api + .getProject("", {}, { id: id }) + .then((res) => res.data as ProjectType); + + setCurrentProject(project); } } catch (error) { console.log(error); From d39ff272c8f8bd3df399b4131708c9da9623247c Mon Sep 17 00:00:00 2001 From: jose-fully-ported <141160579+jose-fully-ported@users.noreply.github.com> Date: Mon, 11 Sep 2023 17:02:21 -0400 Subject: [PATCH 05/59] [POR-1503] simplify retrieval of launchdarkly flags (#3539) --- internal/models/feature_flag.go | 96 --------------------------------- internal/models/project.go | 69 +++++++++++++++++++----- 2 files changed, 55 insertions(+), 110 deletions(-) delete mode 100644 internal/models/feature_flag.go diff --git a/internal/models/feature_flag.go b/internal/models/feature_flag.go deleted file mode 100644 index 3ca1b5fbd1a..00000000000 --- a/internal/models/feature_flag.go +++ /dev/null @@ -1,96 +0,0 @@ -package models - -import ( - "fmt" - - "github.com/launchdarkly/go-sdk-common/v3/ldcontext" - "github.com/porter-dev/porter/internal/features" -) - -func getAPITokensEnabled(context ldcontext.Context, client *features.Client) bool { - defaultValue := false - value, _ := client.BoolVariation("api_tokens_enabled", context, defaultValue) - return value -} - -func getAzureEnabled(context ldcontext.Context, client *features.Client) bool { - defaultValue := false - value, _ := client.BoolVariation("azure_enabled", context, defaultValue) - return value -} - -func getCapiProvisionerEnabled(context ldcontext.Context, client *features.Client) bool { - defaultValue := true - value, _ := client.BoolVariation("capi_provisioner_enabled", context, defaultValue) - return value -} - -func getEnableReprovision(context ldcontext.Context, client *features.Client) bool { - defaultValue := false - value, _ := client.BoolVariation("enable_reprovision", context, defaultValue) - return value -} - -func getFullAddOns(context ldcontext.Context, client *features.Client) bool { - defaultValue := false - value, _ := client.BoolVariation("full_add_ons", context, defaultValue) - return value -} - -func getHelmValuesEnabled(context ldcontext.Context, client *features.Client) bool { - defaultValue := false - value, _ := client.BoolVariation("helm_values_enabled", context, defaultValue) - return value -} - -func getManagedInfraEnabled(context ldcontext.Context, client *features.Client) bool { - defaultValue := false - value, _ := client.BoolVariation("managed_infra_enabled", context, defaultValue) - return value -} - -func getMultiCluster(context ldcontext.Context, client *features.Client) bool { - defaultValue := false - value, _ := client.BoolVariation("multi_cluster", context, defaultValue) - return value -} - -func getPreviewEnvsEnabled(context ldcontext.Context, client *features.Client) bool { - defaultValue := false - value, _ := client.BoolVariation("preview_envs_enabled", context, defaultValue) - return value -} - -func getRdsDatabasesEnabled(context ldcontext.Context, client *features.Client) bool { - defaultValue := false - value, _ := client.BoolVariation("rds_databases_enabled", context, defaultValue) - return value -} - -func getSimplifiedViewEnabled(context ldcontext.Context, client *features.Client) bool { - defaultValue := true - value, _ := client.BoolVariation("simplified_view_enabled", context, defaultValue) - return value -} - -func getStacksEnabled(context ldcontext.Context, client *features.Client) bool { - defaultValue := false - value, _ := client.BoolVariation("stacks_enabled", context, defaultValue) - return value -} - -func getValidateApplyV2(context ldcontext.Context, client *features.Client) bool { - defaultValue := false - value, _ := client.BoolVariation("validate_apply_v2", context, defaultValue) - return value -} - -func getProjectContext(projectID uint, projectName string) ldcontext.Context { - projectIdentifier := fmt.Sprintf("project-%d", projectID) - launchDarklyName := fmt.Sprintf("%s: %s", projectIdentifier, projectName) - return ldcontext.NewBuilder(projectIdentifier). - Kind("project"). - Name(launchDarklyName). - SetInt("project_id", int(projectID)). - Build() -} diff --git a/internal/models/project.go b/internal/models/project.go index 66a5ff9d3ea..6697e21d2a2 100644 --- a/internal/models/project.go +++ b/internal/models/project.go @@ -1,13 +1,33 @@ package models import ( + "fmt" + "gorm.io/gorm" + "github.com/launchdarkly/go-sdk-common/v3/ldcontext" "github.com/porter-dev/porter/api/types" "github.com/porter-dev/porter/internal/features" ints "github.com/porter-dev/porter/internal/models/integrations" ) +// ProjectFeatureFlags keeps track of all project-related feature flags +var ProjectFeatureFlags = map[string]bool{ + "api_tokens_enabled": false, + "azure_enabled": false, + "capi_provisioner_enabled": true, + "enable_reprovision": false, + "full_add_ons": false, + "helm_values_enabled": false, + "managed_infra_enabled": false, + "multi_cluster": false, + "preview_envs_enabled": false, + "rds_databases_enabled": false, + "simplified_view_enabled": true, + "stacks_enabled": false, + "validate_apply_v2": false, +} + type ProjectPlan string const ( @@ -74,6 +94,18 @@ type Project struct { EnableReprovision bool `gorm:"default:false"` } +// GetFeatureFlag calls launchdarkly for the specified flag +// and returns the configured value +func (p *Project) GetFeatureFlag(flagName string, launchDarklyClient *features.Client) bool { + projectID := p.ID + projectName := p.Name + ldContext := getProjectContext(projectID, projectName) + + defaultValue := ProjectFeatureFlags[flagName] + value, _ := launchDarklyClient.BoolVariation(flagName, ldContext, defaultValue) + return value +} + // ToProjectType generates an external types.Project to be shared over REST func (p *Project) ToProjectType(launchDarklyClient *features.Client) types.Project { roles := make([]*types.Role, 0) @@ -84,26 +116,25 @@ func (p *Project) ToProjectType(launchDarklyClient *features.Client) types.Proje projectID := p.ID projectName := p.Name - ldContext := getProjectContext(projectID, projectName) return types.Project{ ID: projectID, Name: projectName, Roles: roles, - PreviewEnvsEnabled: getPreviewEnvsEnabled(ldContext, launchDarklyClient), - RDSDatabasesEnabled: getRdsDatabasesEnabled(ldContext, launchDarklyClient), - ManagedInfraEnabled: getManagedInfraEnabled(ldContext, launchDarklyClient), - StacksEnabled: getStacksEnabled(ldContext, launchDarklyClient), - APITokensEnabled: getAPITokensEnabled(ldContext, launchDarklyClient), - CapiProvisionerEnabled: getCapiProvisionerEnabled(ldContext, launchDarklyClient), - SimplifiedViewEnabled: getSimplifiedViewEnabled(ldContext, launchDarklyClient), - AzureEnabled: getAzureEnabled(ldContext, launchDarklyClient), - HelmValuesEnabled: getHelmValuesEnabled(ldContext, launchDarklyClient), - MultiCluster: getMultiCluster(ldContext, launchDarklyClient), - EnableReprovision: getEnableReprovision(ldContext, launchDarklyClient), - ValidateApplyV2: getValidateApplyV2(ldContext, launchDarklyClient), - FullAddOns: getFullAddOns(ldContext, launchDarklyClient), + PreviewEnvsEnabled: p.GetFeatureFlag("preview_envs_enabled", launchDarklyClient), + RDSDatabasesEnabled: p.GetFeatureFlag("rds_databases_enabled", launchDarklyClient), + ManagedInfraEnabled: p.GetFeatureFlag("managed_infra_enabled", launchDarklyClient), + StacksEnabled: p.GetFeatureFlag("stacks_enabled", launchDarklyClient), + APITokensEnabled: p.GetFeatureFlag("api_tokens_enabled", launchDarklyClient), + CapiProvisionerEnabled: p.GetFeatureFlag("capi_provisioner_enabled", launchDarklyClient), + SimplifiedViewEnabled: p.GetFeatureFlag("simplified_view_enabled", launchDarklyClient), + AzureEnabled: p.GetFeatureFlag("azure_enabled", launchDarklyClient), + HelmValuesEnabled: p.GetFeatureFlag("helm_values_enabled", launchDarklyClient), + MultiCluster: p.GetFeatureFlag("multi_cluster", launchDarklyClient), + EnableReprovision: p.GetFeatureFlag("enable_reprovision", launchDarklyClient), + ValidateApplyV2: p.GetFeatureFlag("validate_apply_v2", launchDarklyClient), + FullAddOns: p.GetFeatureFlag("full_add_ons", launchDarklyClient), } } @@ -139,3 +170,13 @@ func (p *Project) ToProjectListType() *types.ProjectList { FullAddOns: p.FullAddOns, } } + +func getProjectContext(projectID uint, projectName string) ldcontext.Context { + projectIdentifier := fmt.Sprintf("project-%d", projectID) + launchDarklyName := fmt.Sprintf("%s: %s", projectIdentifier, projectName) + return ldcontext.NewBuilder(projectIdentifier). + Kind("project"). + Name(launchDarklyName). + SetInt("project_id", int(projectID)). + Build() +} From 9a5787380a13394cd864a54bec4e83e3339bade1 Mon Sep 17 00:00:00 2001 From: sdess09 <37374498+sdess09@users.noreply.github.com> Date: Mon, 11 Sep 2023 17:45:02 -0400 Subject: [PATCH 06/59] Fix project selection routing (#3543) --- dashboard/src/main/home/sidebar/ProjectSelectionModal.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dashboard/src/main/home/sidebar/ProjectSelectionModal.tsx b/dashboard/src/main/home/sidebar/ProjectSelectionModal.tsx index b34e656cc76..9247f7da156 100644 --- a/dashboard/src/main/home/sidebar/ProjectSelectionModal.tsx +++ b/dashboard/src/main/home/sidebar/ProjectSelectionModal.tsx @@ -103,10 +103,12 @@ const ProjectSelectionModal: React.FC = ({ const clusters_list = await updateClusterList(project.id); if (clusters_list?.length > 0) { setCurrentCluster(clusters_list[0]); - pushFiltered(props, "/dashboard", ["project_id"]); + setCurrentProject(project, () => { + pushFiltered(props, "/dashboard", [], { project_id: project.id }); + }); } else { setCurrentProject(project, () => { - pushFiltered(props, "/dashboard", ["project_id"]); + pushFiltered(props, "/dashboard", [], { project_id: project.id }); }); } closeModal(); From 386f9a086b23afee0e958dda512252ca23385052 Mon Sep 17 00:00:00 2001 From: Feroze Mohideen Date: Tue, 12 Sep 2023 10:36:58 -0400 Subject: [PATCH 07/59] Modal now shows up if porter.yaml is not found in create flow (#3544) --- dashboard/src/lib/hooks/usePorterYaml.ts | 40 ++++++--- .../app-view/LatestRevisionContext.tsx | 2 +- .../app-dashboard/create-app/CreateApp.tsx | 31 +++++-- .../create-app/PorterYamlModal.tsx | 82 +++++++++++++++++++ 4 files changed, 136 insertions(+), 19 deletions(-) create mode 100644 dashboard/src/main/home/app-dashboard/create-app/PorterYamlModal.tsx diff --git a/dashboard/src/lib/hooks/usePorterYaml.ts b/dashboard/src/lib/hooks/usePorterYaml.ts index e99ba9a7945..f5ecf05ee25 100644 --- a/dashboard/src/lib/hooks/usePorterYaml.ts +++ b/dashboard/src/lib/hooks/usePorterYaml.ts @@ -9,13 +9,15 @@ import { z } from "zod"; type PorterYamlStatus = | { - loading: true; - detectedServices: null; - } + loading: true; + detectedServices: null; + porterYamlFound: boolean; + } | { - detectedServices: DetectedServices | null; - loading: false; - }; + detectedServices: DetectedServices | null; + loading: false; + porterYamlFound: boolean; + }; /* * @@ -28,7 +30,7 @@ export const usePorterYaml = ({ source, useDefaults = true, }: { - source: SourceOptions | null; + source: SourceOptions & { type: "github" } | null; useDefaults?: boolean; }): PorterYamlStatus => { const { currentProject, currentCluster } = useContext(Context); @@ -36,6 +38,7 @@ export const usePorterYaml = ({ detectedServices, setDetectedServices, ] = useState(null); + const [porterYamlFound, setPorterYamlFound] = useState(false); const { data, status } = useQuery( [ @@ -43,14 +46,16 @@ export const usePorterYaml = ({ currentProject?.id, source?.git_branch, source?.git_repo_name, + source?.porter_yaml_path, ], async () => { - if (!currentProject) { - return; - } - if (source?.type !== "github") { + setPorterYamlFound(false); + + if (!currentProject || !source) { return; } + + const res = await api.getPorterYamlContents( "", { @@ -66,6 +71,7 @@ export const usePorterYaml = ({ } ); + setPorterYamlFound(true); return z.string().parseAsync(res.data); }, { @@ -73,7 +79,14 @@ export const usePorterYaml = ({ source?.type === "github" && Boolean(source.git_repo_name) && Boolean(source.git_branch), - retry: false, + retry: (_failureCount, error) => { + if (error.response.data?.error?.includes("404")) { + setPorterYamlFound(false); + return false; + } + return true; + }, + refetchOnWindowFocus: false, } ); @@ -144,6 +157,7 @@ export const usePorterYaml = ({ return { loading: false, detectedServices: null, + porterYamlFound: false, }; } @@ -151,11 +165,13 @@ export const usePorterYaml = ({ return { loading: true, detectedServices: null, + porterYamlFound: true, }; } return { detectedServices, loading: false, + porterYamlFound, }; }; diff --git a/dashboard/src/main/home/app-dashboard/app-view/LatestRevisionContext.tsx b/dashboard/src/main/home/app-dashboard/app-view/LatestRevisionContext.tsx index 2347f41b108..8dacfe1d9ea 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/LatestRevisionContext.tsx +++ b/dashboard/src/main/home/app-dashboard/app-view/LatestRevisionContext.tsx @@ -144,7 +144,7 @@ export const LatestRevisionProvider = ({ }, [porterApp]); const { loading: porterYamlLoading, detectedServices } = usePorterYaml({ - source: latestSource, + source: latestSource?.type === "github" ? latestSource : null, useDefaults: false, }); diff --git a/dashboard/src/main/home/app-dashboard/create-app/CreateApp.tsx b/dashboard/src/main/home/app-dashboard/create-app/CreateApp.tsx index 4914e1b7948..0b58ca61654 100644 --- a/dashboard/src/main/home/app-dashboard/create-app/CreateApp.tsx +++ b/dashboard/src/main/home/app-dashboard/create-app/CreateApp.tsx @@ -43,6 +43,7 @@ import { useAppAnalytics } from "lib/hooks/useAppAnalytics"; import { useAppValidation } from "lib/hooks/useAppValidation"; import { useQuery } from "@tanstack/react-query"; import { z } from "zod"; +import PorterYamlModal from "./PorterYamlModal"; type CreateAppProps = {} & RouteComponentProps; @@ -54,6 +55,7 @@ const CreateApp: React.FC = ({ history }) => { count: number; }>({ detected: false, count: 0 }); const [showGHAModal, setShowGHAModal] = React.useState(false); + const [userHasSeenNoPorterYamlFoundModal, setUserHasSeenNoPorterYamlFoundModal] = React.useState(false); const [ validatedAppProto, @@ -130,7 +132,7 @@ const CreateApp: React.FC = ({ history }) => { const build = watch("app.build"); const image = watch("source.image"); const services = watch("app.services"); - const { detectedServices: servicesFromYaml } = usePorterYaml({ source }); + const { detectedServices: servicesFromYaml, porterYamlFound } = usePorterYaml({ source: source?.type === "github" ? source : null }); const deploymentTarget = useDefaultDeploymentTarget(); const { updateAppStep } = useAppAnalytics(name); const { validateApp } = useAppValidation({ @@ -421,11 +423,28 @@ const CreateApp: React.FC = ({ history }) => { {source?.type ? ( source.type === "github" ? ( - + <> + + {!userHasSeenNoPorterYamlFoundModal && !porterYamlFound && + ( + setUserHasSeenNoPorterYamlFoundModal(true)} + setPorterYamlPath={(porterYamlPath) => { + onChange(porterYamlPath); + }} + porterYamlPath={value} + /> + )} + /> + } + ) : ( ) diff --git a/dashboard/src/main/home/app-dashboard/create-app/PorterYamlModal.tsx b/dashboard/src/main/home/app-dashboard/create-app/PorterYamlModal.tsx new file mode 100644 index 00000000000..973676b4640 --- /dev/null +++ b/dashboard/src/main/home/app-dashboard/create-app/PorterYamlModal.tsx @@ -0,0 +1,82 @@ +import React, { useState } from "react"; +import styled from "styled-components"; + +import Link from "components/porter/Link"; +import Spacer from "components/porter/Spacer"; +import Modal from "components/porter/Modal"; +import Text from "components/porter/Text"; +import Input from "components/porter/Input"; +import Button from "components/porter/Button"; + +type Props = { + close: () => void; + setPorterYamlPath: (path: string) => void; + porterYamlPath: string; +} + +const PorterYamlModal: React.FC = ({ close, setPorterYamlPath, porterYamlPath }) => { + const [possiblePorterYamlPath, setPossiblePorterYamlPath] = useState(""); + const [showModal, setShowModal] = useState(true); + + return showModal ? ( + setShowModal(false)}> +
+ No porter.yaml detected at {porterYamlPath} + + + + We were unable to find a porter.yaml file in your repository. + + + + Although not required, we + recommend that you add a porter.yaml file to the root of your repository, + or you may specify its path here. + + + + Using porter.yaml + + +
+ + Path to porter.yaml from repository root (i.e. starting with ./): + + + +
+ + +
+
+ ) : null; +}; + +export default PorterYamlModal; + +const Code = styled.span` + font-family: monospace; +`; \ No newline at end of file From 2bc8563ee446e183ddf4cdbcbdf20a05479bbce9 Mon Sep 17 00:00:00 2001 From: Feroze Mohideen Date: Tue, 12 Sep 2023 11:03:38 -0400 Subject: [PATCH 08/59] [POR-1675] filter out porter app events that are related to cpu or memory overages (#3546) Co-authored-by: sdess09 <37374498+sdess09@users.noreply.github.com> --- .../expanded-app/activity-feed/ActivityFeed.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/ActivityFeed.tsx b/dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/ActivityFeed.tsx index 9d8db8c3737..ceea8a8275a 100644 --- a/dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/ActivityFeed.tsx +++ b/dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/ActivityFeed.tsx @@ -4,8 +4,6 @@ import styled from "styled-components"; import api from "shared/api"; import { Context } from "shared/Context"; -import refresh from "assets/refresh.png"; - import Text from "components/porter/Text"; import EventCard from "./events/cards/EventCard"; @@ -17,8 +15,6 @@ import { feedDate } from "shared/string_utils"; import Pagination from "components/porter/Pagination"; import _ from "lodash"; import Button from "components/porter/Button"; -import Icon from "components/porter/Icon"; -import Container from "components/porter/Container"; import { PorterAppEvent, PorterAppEventType } from "./events/types"; type Props = { @@ -41,6 +37,11 @@ const ActivityFeed: React.FC = ({ chart, stackName, appData }) => { const [isPorterAgentInstalling, setIsPorterAgentInstalling] = useState(false); const [shouldAnimate, setShouldAnimate] = useState(true); + // remove this filter when https://linear.app/porter/issue/POR-1676/disable-porter-agent-code-for-cpu-alerts is resolved + const isFilteredAppEvent = (event: PorterAppEvent) => { + return event.type === PorterAppEventType.APP_EVENT && (event.metadata?.short_summary?.includes("requesting more memory than is available") || event.metadata?.short_summary?.includes("requesting more CPU than is available")); + } + const getEvents = async () => { setLoading(true) if (!currentProject || !currentCluster) { @@ -61,7 +62,7 @@ const ActivityFeed: React.FC = ({ chart, stackName, appData }) => { ); setNumPages(res.data.num_pages); - setEvents(res.data.events?.map((event: any) => PorterAppEvent.toPorterAppEvent(event)) ?? []); + setEvents(res.data.events?.map((event: any) => PorterAppEvent.toPorterAppEvent(event)).filter(!isFilteredAppEvent) ?? []); } catch (err) { setError(err); } finally { @@ -95,7 +96,7 @@ const ActivityFeed: React.FC = ({ chart, stackName, appData }) => { ); setError(undefined) setNumPages(res.data.num_pages); - setEvents(res.data.events?.map((event: any) => PorterAppEvent.toPorterAppEvent(event)) ?? []); + setEvents(res.data.events?.map((event: any) => PorterAppEvent.toPorterAppEvent(event)).filter(!isFilteredAppEvent) ?? []); } catch (err) { setError(err); } From 41651dba0ce97ea14bfc9f89197107fa03b307bb Mon Sep 17 00:00:00 2001 From: ianedwards Date: Tue, 12 Sep 2023 11:35:13 -0400 Subject: [PATCH 09/59] use name from yaml if it exists (#3545) --- dashboard/src/lib/hooks/usePorterYaml.ts | 31 ++++++---- dashboard/src/lib/porter-apps/index.ts | 62 +++++++++++-------- .../home/app-dashboard/app-view/AppView.tsx | 11 +--- .../app-dashboard/create-app/CreateApp.tsx | 45 +++++++++----- 4 files changed, 88 insertions(+), 61 deletions(-) diff --git a/dashboard/src/lib/hooks/usePorterYaml.ts b/dashboard/src/lib/hooks/usePorterYaml.ts index f5ecf05ee25..bf78e584342 100644 --- a/dashboard/src/lib/hooks/usePorterYaml.ts +++ b/dashboard/src/lib/hooks/usePorterYaml.ts @@ -1,7 +1,7 @@ import { PorterApp } from "@porter-dev/api-contracts"; import { useQuery } from "@tanstack/react-query"; import { SourceOptions, serviceOverrides } from "lib/porter-apps"; -import { ClientService, DetectedServices } from "lib/porter-apps/services"; +import { DetectedServices } from "lib/porter-apps/services"; import { useCallback, useContext, useEffect, useState } from "react"; import { Context } from "shared/Context"; import api from "shared/api"; @@ -9,15 +9,17 @@ import { z } from "zod"; type PorterYamlStatus = | { - loading: true; - detectedServices: null; - porterYamlFound: boolean; - } + loading: true; + detectedName: null; + detectedServices: null; + porterYamlFound: boolean; + } | { - detectedServices: DetectedServices | null; - loading: false; - porterYamlFound: boolean; - }; + detectedServices: DetectedServices | null; + detectedName: string | null; + loading: false; + porterYamlFound: boolean; + }; /* * @@ -30,7 +32,7 @@ export const usePorterYaml = ({ source, useDefaults = true, }: { - source: SourceOptions & { type: "github" } | null; + source: (SourceOptions & { type: "github" }) | null; useDefaults?: boolean; }): PorterYamlStatus => { const { currentProject, currentCluster } = useContext(Context); @@ -38,6 +40,7 @@ export const usePorterYaml = ({ detectedServices, setDetectedServices, ] = useState(null); + const [detectedName, setDetectedName] = useState(null); const [porterYamlFound, setPorterYamlFound] = useState(false); const { data, status } = useQuery( @@ -55,7 +58,6 @@ export const usePorterYaml = ({ return; } - const res = await api.getPorterYamlContents( "", { @@ -128,6 +130,10 @@ export const usePorterYaml = ({ predeploy, }); } + + if (proto.name) { + setDetectedName(proto.name); + } } catch (err) { // silent failure for now } @@ -156,6 +162,7 @@ export const usePorterYaml = ({ if (source?.type !== "github") { return { loading: false, + detectedName: null, detectedServices: null, porterYamlFound: false, }; @@ -164,6 +171,7 @@ export const usePorterYaml = ({ if (status === "loading") { return { loading: true, + detectedName: null, detectedServices: null, porterYamlFound: true, }; @@ -171,6 +179,7 @@ export const usePorterYaml = ({ return { detectedServices, + detectedName, loading: false, porterYamlFound, }; diff --git a/dashboard/src/lib/porter-apps/index.ts b/dashboard/src/lib/porter-apps/index.ts index 04f1f37a51d..09419facef4 100644 --- a/dashboard/src/lib/porter-apps/index.ts +++ b/dashboard/src/lib/porter-apps/index.ts @@ -66,7 +66,10 @@ export const deletionValidator = z.object({ // clientAppValidator is the representation of a Porter app on the client, and is used to validate inputs for app setting fields export const clientAppValidator = z.object({ - name: z.string().min(1), + name: z.object({ + readOnly: z.boolean(), + value: z.string(), + }), services: serviceValidator.array(), predeploy: serviceValidator.array().optional(), env: z.record(z.string(), z.string()).default({}), @@ -164,20 +167,19 @@ const clientBuildToProto = (build: BuildOptions) => { export function clientAppToProto(data: PorterAppFormData): PorterApp { const { app, source } = data; - const services = app.services - .reduce((acc: Record, svc) => { - acc[svc.name.value] = serviceProto(serializeService(svc)); - return acc; - }, {}); + const services = app.services.reduce((acc: Record, svc) => { + acc[svc.name.value] = serviceProto(serializeService(svc)); + return acc; + }, {}); - const predeploy = app.predeploy?.[0] + const predeploy = app.predeploy?.[0]; const proto = match(source) .with( { type: "github" }, () => new PorterApp({ - name: app.name, + name: app.name.value, services, env: app.env, build: clientBuildToProto(app.build), @@ -190,7 +192,7 @@ export function clientAppToProto(data: PorterAppFormData): PorterApp { { type: "docker-registry" }, (src) => new PorterApp({ - name: app.name, + name: app.name.value, services, env: app.env, image: { @@ -275,17 +277,22 @@ export function clientAppFromProto( const predeployList = []; if (proto.predeploy) { - predeployList.push(deserializeService({ - service: serializedServiceFromProto({ - name: "pre-deploy", - service: proto.predeploy, - isPredeploy: true, + predeployList.push( + deserializeService({ + service: serializedServiceFromProto({ + name: "pre-deploy", + service: proto.predeploy, + isPredeploy: true, + }), }) - })) + ); } if (!overrides?.predeploy) { return { - name: proto.name, + name: { + readOnly: true, + value: proto.name, + }, services, predeploy: predeployList, env: proto.env, @@ -300,18 +307,23 @@ export function clientAppFromProto( const predeployOverrides = serializeService(overrides.predeploy); const predeploy = proto.predeploy - ? [deserializeService({ - service: serializedServiceFromProto({ - name: "pre-deploy", - service: proto.predeploy, - isPredeploy: true, - }), - override: predeployOverrides, - })] + ? [ + deserializeService({ + service: serializedServiceFromProto({ + name: "pre-deploy", + service: proto.predeploy, + isPredeploy: true, + }), + override: predeployOverrides, + }), + ] : undefined; return { - name: proto.name, + name: { + readOnly: true, + value: proto.name, + }, services, predeploy, env: proto.env, diff --git a/dashboard/src/main/home/app-dashboard/app-view/AppView.tsx b/dashboard/src/main/home/app-dashboard/app-view/AppView.tsx index 828fe1e1fd4..0009216e364 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/AppView.tsx +++ b/dashboard/src/main/home/app-dashboard/app-view/AppView.tsx @@ -27,15 +27,6 @@ export const porterAppValidator = z.object({ }); export type PorterAppRecord = z.infer; -// Buildpack icons -const icons = [ - "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/ruby/ruby-plain.svg", - "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/nodejs/nodejs-plain.svg", - "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/python/python-plain.svg", - "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/go/go-original-wordmark.svg", - web, -]; - // commented out tabs are not yet implemented // will be included as support is available based on data from app revisions rather than helm releases const validTabs = [ @@ -103,4 +94,4 @@ const StyledExpandedApp = styled.div` opacity: 1; } } -`; \ No newline at end of file +`; diff --git a/dashboard/src/main/home/app-dashboard/create-app/CreateApp.tsx b/dashboard/src/main/home/app-dashboard/create-app/CreateApp.tsx index 0b58ca61654..eb742cb9976 100644 --- a/dashboard/src/main/home/app-dashboard/create-app/CreateApp.tsx +++ b/dashboard/src/main/home/app-dashboard/create-app/CreateApp.tsx @@ -98,7 +98,10 @@ const CreateApp: React.FC = ({ history }) => { reValidateMode: "onSubmit", defaultValues: { app: { - name: "", + name: { + value: "", + readOnly: false, + }, build: { method: "pack", context: "./", @@ -113,7 +116,7 @@ const CreateApp: React.FC = ({ history }) => { }, deletions: { serviceNames: [], - } + }, }, }); const { @@ -132,9 +135,9 @@ const CreateApp: React.FC = ({ history }) => { const build = watch("app.build"); const image = watch("source.image"); const services = watch("app.services"); - const { detectedServices: servicesFromYaml, porterYamlFound } = usePorterYaml({ source: source?.type === "github" ? source : null }); + const { detectedServices: servicesFromYaml, porterYamlFound, detectedName } = usePorterYaml({ source: source?.type === "github" ? source : null }); const deploymentTarget = useDefaultDeploymentTarget(); - const { updateAppStep } = useAppAnalytics(name); + const { updateAppStep } = useAppAnalytics(name.value); const { validateApp } = useAppValidation({ deploymentTargetID: deploymentTarget?.deployment_target_id, creating: true, @@ -299,7 +302,7 @@ const CreateApp: React.FC = ({ history }) => { }, [isValidating, isDeploying, deployError, errors]); const submitDisabled = useMemo(() => { - return !name || !source || services.length === 0; + return !name || !source || services?.length === 0; }, [name, source, services?.length]); // reset services when source changes @@ -345,17 +348,21 @@ const CreateApp: React.FC = ({ history }) => { count: 0, }); } - }, [servicesFromYaml, detectedServices.detected]); + + if (detectedName) { + setValue("app.name", { value: detectedName, readOnly: true }); + } + }, [servicesFromYaml, detectedName, detectedServices.detected]); useEffect(() => { - if (porterApps.includes(name)) { - setError("app.name", { + if (porterApps.includes(name.value)) { + setError("app.name.value", { message: "An app with this name already exists", }); } else { - clearErrors("app.name"); + clearErrors("app.name.value"); } - }, [porterApps, name]); + }, [porterApps, name.value]); if (!currentProject || !currentCluster) { return null; @@ -389,7 +396,11 @@ const CreateApp: React.FC = ({ history }) => { placeholder="ex: academic-sophon" type="text" error={errors.app?.name?.message} - {...register("app.name")} + disabled={name.readOnly} + disabledTooltip={ + "You may only edit this field in your porter.yaml." + } + {...register("app.name.value")} /> , <> @@ -471,15 +482,19 @@ const CreateApp: React.FC = ({ history }) => { } > {detectedServices.count > 0 - ? `Detected ${detectedServices.count} service${detectedServices.count > 1 ? "s" : "" - } from porter.yaml.` + ? `Detected ${detectedServices.count} service${ + detectedServices.count > 1 ? "s" : "" + } from porter.yaml.` : `Could not detect any services from porter.yaml. Make sure it exists in the root of your repo.`} )} - + , <> Environment variables (optional) @@ -536,7 +551,7 @@ const CreateApp: React.FC = ({ history }) => { githubRepoOwner={source.git_repo_name.split("/")[0]} githubRepoName={source.git_repo_name.split("/")[1]} branch={source.git_branch} - stackName={name} + stackName={name.value} projectId={currentProject.id} clusterId={currentCluster.id} deployPorterApp={() => From 988883950c5469ebcc1352f27cb4ec1c5bbf42da Mon Sep 17 00:00:00 2001 From: Feroze Mohideen Date: Tue, 12 Sep 2023 11:43:54 -0400 Subject: [PATCH 10/59] fix filter (#3548) --- .../expanded-app/activity-feed/ActivityFeed.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/ActivityFeed.tsx b/dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/ActivityFeed.tsx index ceea8a8275a..95dd07f8f9b 100644 --- a/dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/ActivityFeed.tsx +++ b/dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/ActivityFeed.tsx @@ -38,8 +38,8 @@ const ActivityFeed: React.FC = ({ chart, stackName, appData }) => { const [shouldAnimate, setShouldAnimate] = useState(true); // remove this filter when https://linear.app/porter/issue/POR-1676/disable-porter-agent-code-for-cpu-alerts is resolved - const isFilteredAppEvent = (event: PorterAppEvent) => { - return event.type === PorterAppEventType.APP_EVENT && (event.metadata?.short_summary?.includes("requesting more memory than is available") || event.metadata?.short_summary?.includes("requesting more CPU than is available")); + const isNotFilteredAppEvent = (event: PorterAppEvent) => { + return !(event.type === PorterAppEventType.APP_EVENT && (event.metadata?.short_summary?.includes("requesting more memory than is available") || event.metadata?.short_summary?.includes("requesting more CPU than is available"))); } const getEvents = async () => { @@ -62,7 +62,7 @@ const ActivityFeed: React.FC = ({ chart, stackName, appData }) => { ); setNumPages(res.data.num_pages); - setEvents(res.data.events?.map((event: any) => PorterAppEvent.toPorterAppEvent(event)).filter(!isFilteredAppEvent) ?? []); + setEvents(res.data.events?.map((event: any) => PorterAppEvent.toPorterAppEvent(event)).filter(isNotFilteredAppEvent) ?? []); } catch (err) { setError(err); } finally { @@ -96,7 +96,7 @@ const ActivityFeed: React.FC = ({ chart, stackName, appData }) => { ); setError(undefined) setNumPages(res.data.num_pages); - setEvents(res.data.events?.map((event: any) => PorterAppEvent.toPorterAppEvent(event)).filter(!isFilteredAppEvent) ?? []); + setEvents(res.data.events?.map((event: any) => PorterAppEvent.toPorterAppEvent(event)).filter(isNotFilteredAppEvent) ?? []); } catch (err) { setError(err); } From 64dc1779d17de9b5467756dcf030d3dff784685c Mon Sep 17 00:00:00 2001 From: sdess09 <37374498+sdess09@users.noreply.github.com> Date: Tue, 12 Sep 2023 12:05:44 -0400 Subject: [PATCH 11/59] Update GKE cluster name length to be only 15 characters (#3547) --- dashboard/src/components/GCPProvisionerSettings.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dashboard/src/components/GCPProvisionerSettings.tsx b/dashboard/src/components/GCPProvisionerSettings.tsx index a1fd8fb13c5..26838f7ab7b 100644 --- a/dashboard/src/components/GCPProvisionerSettings.tsx +++ b/dashboard/src/components/GCPProvisionerSettings.tsx @@ -336,9 +336,9 @@ const GCPProvisionerSettings: React.FC = (props) => { currentCluster?.status === "UPDATING_UNAVAILABLE") ); setClusterName( - `${currentProject.name}-cluster-${Math.random() + `${currentProject.name.substring(0, 10)}-${Math.random() .toString(36) - .substring(2, 8)}` + .substring(2, 6)}` ); }, []); From 9bd424e642cecbca67b9dcaecdc8c1f7040ceae7 Mon Sep 17 00:00:00 2001 From: ianedwards Date: Tue, 12 Sep 2023 12:30:21 -0400 Subject: [PATCH 12/59] fix update and delete issues (#3549) --- .../app-view/AppDataContainer.tsx | 23 +++++++++++-------- .../app-view/tabs/BuildSettings.tsx | 12 ++++++++-- .../app-view/tabs/Environment.tsx | 13 ++++++++++- .../app-dashboard/app-view/tabs/Overview.tsx | 14 ++++++++--- .../app-dashboard/app-view/tabs/Settings.tsx | 3 +++ .../revisions-list/RevisionTableContents.tsx | 5 ++-- 6 files changed, 52 insertions(+), 18 deletions(-) diff --git a/dashboard/src/main/home/app-dashboard/app-view/AppDataContainer.tsx b/dashboard/src/main/home/app-dashboard/app-view/AppDataContainer.tsx index 2594452c99e..50a9d506fc9 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/AppDataContainer.tsx +++ b/dashboard/src/main/home/app-dashboard/app-view/AppDataContainer.tsx @@ -201,16 +201,19 @@ const AppDataContainer: React.FC = ({ tabParam }) => { }); useEffect(() => { - if (servicesFromYaml) { - reset({ - app: clientAppFromProto(latestProto, servicesFromYaml), - source: latestSource, - deletions: { - serviceNames: [], - }, - }); - } - }, [servicesFromYaml, currentTab, latestProto]); + reset({ + app: clientAppFromProto(latestProto, servicesFromYaml), + source: latestSource, + deletions: { + serviceNames: [], + }, + }); + }, [ + servicesFromYaml, + currentTab, + latestProto, + latestRevision.revision_number, + ]); return ( diff --git a/dashboard/src/main/home/app-dashboard/app-view/tabs/BuildSettings.tsx b/dashboard/src/main/home/app-dashboard/app-view/tabs/BuildSettings.tsx index 7de85285774..37ac7dac1e8 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/tabs/BuildSettings.tsx +++ b/dashboard/src/main/home/app-dashboard/app-view/tabs/BuildSettings.tsx @@ -22,7 +22,7 @@ const BuildSettings: React.FC = ({ watch, formState: { isSubmitting, errors }, } = useFormContext(); - const { projectId } = useLatestRevision(); + const { projectId, latestRevision } = useLatestRevision(); const build = watch("app.build"); const source = watch("source"); @@ -59,7 +59,15 @@ const BuildSettings: React.FC = ({ Re-run build and deploy on save - diff --git a/dashboard/src/main/home/app-dashboard/app-view/tabs/Environment.tsx b/dashboard/src/main/home/app-dashboard/app-view/tabs/Environment.tsx index f1cb0d6de0f..b991682b0d9 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/tabs/Environment.tsx +++ b/dashboard/src/main/home/app-dashboard/app-view/tabs/Environment.tsx @@ -6,8 +6,10 @@ import Button from "components/porter/Button"; import Error from "components/porter/Error"; import { useFormContext } from "react-hook-form"; import { PorterAppFormData } from "lib/porter-apps"; +import { useLatestRevision } from "../LatestRevisionContext"; const Environment: React.FC = () => { + const { latestRevision } = useLatestRevision(); const { formState: { isSubmitting, errors }, } = useFormContext(); @@ -31,7 +33,16 @@ const Environment: React.FC = () => { Shared among all services. - diff --git a/dashboard/src/main/home/app-dashboard/app-view/tabs/Overview.tsx b/dashboard/src/main/home/app-dashboard/app-view/tabs/Overview.tsx index 69ce71c7e0d..975687baad4 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/tabs/Overview.tsx +++ b/dashboard/src/main/home/app-dashboard/app-view/tabs/Overview.tsx @@ -15,7 +15,7 @@ import { useLatestRevision } from "../LatestRevisionContext"; const Overview: React.FC = () => { const { formState } = useFormContext(); - const { porterApp, latestProto } = useLatestRevision(); + const { porterApp, latestProto, latestRevision } = useLatestRevision(); const buttonStatus = useMemo(() => { if (formState.isSubmitting) { @@ -52,13 +52,21 @@ const Overview: React.FC = () => { )} Application services - + diff --git a/dashboard/src/main/home/app-dashboard/app-view/tabs/Settings.tsx b/dashboard/src/main/home/app-dashboard/app-view/tabs/Settings.tsx index 21d4ae9c634..f2be491d4d4 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/tabs/Settings.tsx +++ b/dashboard/src/main/home/app-dashboard/app-view/tabs/Settings.tsx @@ -10,8 +10,10 @@ import DeleteApplicationModal from "../../expanded-app/DeleteApplicationModal"; import { useLatestRevision } from "../LatestRevisionContext"; import api from "shared/api"; import { useAppAnalytics } from "lib/hooks/useAppAnalytics"; +import { useQueryClient } from "@tanstack/react-query"; const Settings: React.FC = () => { + const queryClient = useQueryClient(); const history = useHistory(); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const { porterApp, clusterId, projectId } = useLatestRevision(); @@ -62,6 +64,7 @@ const Settings: React.FC = () => { name: porterApp.name, } ); + void queryClient.invalidateQueries(); if (!deleteWorkflow) { return; diff --git a/dashboard/src/main/home/app-dashboard/validate-apply/revisions-list/RevisionTableContents.tsx b/dashboard/src/main/home/app-dashboard/validate-apply/revisions-list/RevisionTableContents.tsx index eaa5d8abc94..89794c39d86 100644 --- a/dashboard/src/main/home/app-dashboard/validate-apply/revisions-list/RevisionTableContents.tsx +++ b/dashboard/src/main/home/app-dashboard/validate-apply/revisions-list/RevisionTableContents.tsx @@ -6,14 +6,12 @@ import { useLatestRevision } from "../../app-view/LatestRevisionContext"; import styled from "styled-components"; import { readableDate } from "shared/string_utils"; import Text from "components/porter/Text"; -import { useGithubWorkflow } from "lib/hooks/useGithubWorkflow"; import { useFormContext } from "react-hook-form"; import { PorterAppFormData, SourceOptions, clientAppFromProto, } from "lib/porter-apps"; -import GHStatusBanner from "./GHStatusBanner"; type RevisionTableContentsProps = { latestRevisionNumber: number; @@ -191,6 +189,9 @@ const RevisionTableContents: React.FC = ({ servicesFromYaml ), source: latestSource, + deletions: { + serviceNames: [], + }, }); setPreviewRevision( isLatestDeployedRevision From 9b763e7aab94094055c5e3d13a503f0bf7977f29 Mon Sep 17 00:00:00 2001 From: Feroze Mohideen Date: Tue, 12 Sep 2023 13:41:55 -0400 Subject: [PATCH 13/59] Fix parse v2 to use provided app name if not found in porter.yaml (#3550) --- dashboard/src/lib/hooks/usePorterYaml.ts | 24 +++++++++---------- .../app-dashboard/create-app/CreateApp.tsx | 9 ++++--- internal/porter_app/parse.go | 2 +- internal/porter_app/v2/yaml.go | 7 +++++- 4 files changed, 22 insertions(+), 20 deletions(-) diff --git a/dashboard/src/lib/hooks/usePorterYaml.ts b/dashboard/src/lib/hooks/usePorterYaml.ts index bf78e584342..8550f68f8f1 100644 --- a/dashboard/src/lib/hooks/usePorterYaml.ts +++ b/dashboard/src/lib/hooks/usePorterYaml.ts @@ -9,17 +9,17 @@ import { z } from "zod"; type PorterYamlStatus = | { - loading: true; - detectedName: null; - detectedServices: null; - porterYamlFound: boolean; - } + loading: true; + detectedName: null; + detectedServices: null; + porterYamlFound: false; + } | { - detectedServices: DetectedServices | null; - detectedName: string | null; - loading: false; - porterYamlFound: boolean; - }; + detectedServices: DetectedServices | null; + detectedName: string | null; + loading: false; + porterYamlFound: boolean; + }; /* * @@ -52,8 +52,6 @@ export const usePorterYaml = ({ source?.porter_yaml_path, ], async () => { - setPorterYamlFound(false); - if (!currentProject || !source) { return; } @@ -173,7 +171,7 @@ export const usePorterYaml = ({ loading: true, detectedName: null, detectedServices: null, - porterYamlFound: true, + porterYamlFound: false, }; } diff --git a/dashboard/src/main/home/app-dashboard/create-app/CreateApp.tsx b/dashboard/src/main/home/app-dashboard/create-app/CreateApp.tsx index eb742cb9976..32c6501fb5b 100644 --- a/dashboard/src/main/home/app-dashboard/create-app/CreateApp.tsx +++ b/dashboard/src/main/home/app-dashboard/create-app/CreateApp.tsx @@ -135,7 +135,7 @@ const CreateApp: React.FC = ({ history }) => { const build = watch("app.build"); const image = watch("source.image"); const services = watch("app.services"); - const { detectedServices: servicesFromYaml, porterYamlFound, detectedName } = usePorterYaml({ source: source?.type === "github" ? source : null }); + const { detectedServices: servicesFromYaml, porterYamlFound, detectedName, loading: isLoadingPorterYaml } = usePorterYaml({ source: source?.type === "github" ? source : null }); const deploymentTarget = useDefaultDeploymentTarget(); const { updateAppStep } = useAppAnalytics(name.value); const { validateApp } = useAppValidation({ @@ -440,7 +440,7 @@ const CreateApp: React.FC = ({ history }) => { source={source} projectId={currentProject.id} /> - {!userHasSeenNoPorterYamlFoundModal && !porterYamlFound && + {!userHasSeenNoPorterYamlFoundModal && !porterYamlFound && !isLoadingPorterYaml && = ({ history }) => { } > {detectedServices.count > 0 - ? `Detected ${detectedServices.count} service${ - detectedServices.count > 1 ? "s" : "" - } from porter.yaml.` + ? `Detected ${detectedServices.count} service${detectedServices.count > 1 ? "s" : "" + } from porter.yaml.` : `Could not detect any services from porter.yaml. Make sure it exists in the root of your repo.`} diff --git a/internal/porter_app/parse.go b/internal/porter_app/parse.go index 852c324aa5d..5d72bec0935 100644 --- a/internal/porter_app/parse.go +++ b/internal/porter_app/parse.go @@ -41,7 +41,7 @@ func ParseYAML(ctx context.Context, porterYaml []byte, appName string) (*porterv switch version.Version { case PorterYamlVersion_V2: - appProto, err = v2.AppProtoFromYaml(ctx, porterYaml) + appProto, err = v2.AppProtoFromYaml(ctx, porterYaml, appName) if err != nil { return nil, telemetry.Error(ctx, span, err, "error converting v2 yaml to proto") } diff --git a/internal/porter_app/v2/yaml.go b/internal/porter_app/v2/yaml.go index f969e94b2ce..6deed404433 100644 --- a/internal/porter_app/v2/yaml.go +++ b/internal/porter_app/v2/yaml.go @@ -12,7 +12,7 @@ import ( ) // AppProtoFromYaml converts a Porter YAML file into a PorterApp proto object -func AppProtoFromYaml(ctx context.Context, porterYamlBytes []byte) (*porterv1.PorterApp, error) { +func AppProtoFromYaml(ctx context.Context, porterYamlBytes []byte, appName string) (*porterv1.PorterApp, error) { ctx, span := telemetry.NewSpan(ctx, "v2-app-proto-from-yaml") defer span.End() @@ -26,6 +26,11 @@ func AppProtoFromYaml(ctx context.Context, porterYamlBytes []byte) (*porterv1.Po return nil, telemetry.Error(ctx, span, err, "error unmarshaling porter yaml") } + // if the porter yaml is missing a name field, use the app name that is provided in the request + if porterYaml.Name == "" { + porterYaml.Name = appName + } + appProto := &porterv1.PorterApp{ Name: porterYaml.Name, Env: porterYaml.Env, From a343fec5584719ca05df5b31508bc3b35be76bbb Mon Sep 17 00:00:00 2001 From: jose-fully-ported <141160579+jose-fully-ported@users.noreply.github.com> Date: Tue, 12 Sep 2023 14:03:27 -0400 Subject: [PATCH 14/59] [POR-1503] respect ld feature flags across codebase (#3542) --- api/server/authz/preview_environment.go | 2 +- api/server/handlers/api_token/create.go | 2 +- api/server/handlers/api_token/get.go | 2 +- api/server/handlers/api_token/list.go | 2 +- api/server/handlers/api_token/revoke.go | 2 +- api/server/handlers/cluster/create.go | 6 +- .../handlers/cluster/create_candidate.go | 2 +- api/server/handlers/cluster/rename.go | 2 +- .../handlers/cluster/resolve_candidate.go | 2 +- api/server/handlers/cluster/update.go | 2 +- .../gitinstallation/get_porter_yaml.go | 4 +- api/server/handlers/porter_app/apply.go | 2 +- api/server/handlers/porter_app/create_app.go | 2 +- api/server/handlers/porter_app/get.go | 2 +- api/server/handlers/porter_app/parse_yaml.go | 2 +- api/server/handlers/porter_app/validate.go | 2 +- api/server/handlers/project/delete.go | 2 +- .../project_integration/create_aws.go | 2 +- .../project_integration/create_gcp.go | 2 +- .../handlers/project_integration/list_aws.go | 2 +- api/server/handlers/registry/get_token.go | 6 +- api/server/shared/config/loader/loader.go | 2 +- cmd/app/main.go | 2 +- .../enable_cluster_preview_envs/enable.go | 6 +- .../enable_test.go | 14 +- .../helpers_test.go | 3 +- cmd/migrate/keyrotate/helpers_test.go | 3 +- cmd/migrate/main.go | 9 +- .../helpers_test.go | 3 +- cmd/migrate/startup_migrations/global_map.go | 3 +- internal/features/launch_darkly.go | 17 +- internal/kubernetes/resolver/resolver.go | 4 +- internal/models/project.go | 153 +++++++++++++----- internal/registry/registry.go | 6 +- internal/repository/cluster.go | 5 +- internal/repository/gorm/cluster.go | 7 +- internal/repository/gorm/cluster_test.go | 6 +- internal/repository/gorm/helpers_test.go | 3 +- internal/repository/test/cluster.go | 3 + provisioner/server/config/config.go | 13 ++ .../server/handlers/state/create_resource.go | 9 +- 41 files changed, 225 insertions(+), 98 deletions(-) diff --git a/api/server/authz/preview_environment.go b/api/server/authz/preview_environment.go index eef2255155f..6d36473871a 100644 --- a/api/server/authz/preview_environment.go +++ b/api/server/authz/preview_environment.go @@ -38,7 +38,7 @@ func (p *PreviewEnvironmentScopedMiddleware) ServeHTTP(w http.ResponseWriter, r project, _ := r.Context().Value(types.ProjectScope).(*models.Project) cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster) - if !project.PreviewEnvsEnabled { + if !project.GetFeatureFlag(models.PreviewEnvsEnabled, p.config.LaunchDarklyClient) { apierrors.HandleAPIError(p.config.Logger, p.config.Alerter, w, r, apierrors.NewErrForbidden(errPreviewProjectDisabled), true) return diff --git a/api/server/handlers/api_token/create.go b/api/server/handlers/api_token/create.go index 42e542fd121..20b577f6223 100644 --- a/api/server/handlers/api_token/create.go +++ b/api/server/handlers/api_token/create.go @@ -35,7 +35,7 @@ func (p *APITokenCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request user, _ := r.Context().Value(types.UserScope).(*models.User) proj, _ := r.Context().Value(types.ProjectScope).(*models.Project) - if !proj.APITokensEnabled { + if !proj.GetFeatureFlag(models.APITokensEnabled, p.Config().LaunchDarklyClient) { p.HandleAPIError(w, r, apierrors.NewErrForbidden(fmt.Errorf("api token endpoints are not enabled for this project"))) return } diff --git a/api/server/handlers/api_token/get.go b/api/server/handlers/api_token/get.go index bfb7431d3b3..2c8077c6dd1 100644 --- a/api/server/handlers/api_token/get.go +++ b/api/server/handlers/api_token/get.go @@ -33,7 +33,7 @@ func NewAPITokenGetHandler( func (p *APITokenGetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { proj, _ := r.Context().Value(types.ProjectScope).(*models.Project) - if !proj.APITokensEnabled { + if !proj.GetFeatureFlag(models.APITokensEnabled, p.Config().LaunchDarklyClient) { p.HandleAPIError(w, r, apierrors.NewErrForbidden(fmt.Errorf("api token endpoints are not enabled for this project"))) return } diff --git a/api/server/handlers/api_token/list.go b/api/server/handlers/api_token/list.go index 0f7569c6769..7cdfe7bd85b 100644 --- a/api/server/handlers/api_token/list.go +++ b/api/server/handlers/api_token/list.go @@ -29,7 +29,7 @@ func NewAPITokenListHandler( func (p *APITokenListHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { proj, _ := r.Context().Value(types.ProjectScope).(*models.Project) - if !proj.APITokensEnabled { + if !proj.GetFeatureFlag(models.APITokensEnabled, p.Config().LaunchDarklyClient) { p.HandleAPIError(w, r, apierrors.NewErrForbidden(fmt.Errorf("api token endpoints are not enabled for this project"))) return } diff --git a/api/server/handlers/api_token/revoke.go b/api/server/handlers/api_token/revoke.go index de831b8ffb6..946d4fae2ea 100644 --- a/api/server/handlers/api_token/revoke.go +++ b/api/server/handlers/api_token/revoke.go @@ -32,7 +32,7 @@ func NewAPITokenRevokeHandler( func (p *APITokenRevokeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { proj, _ := r.Context().Value(types.ProjectScope).(*models.Project) - if !proj.APITokensEnabled { + if !proj.GetFeatureFlag(models.APITokensEnabled, p.Config().LaunchDarklyClient) { p.HandleAPIError(w, r, apierrors.NewErrForbidden(fmt.Errorf("api token endpoints are not enabled for this project"))) return } diff --git a/api/server/handlers/cluster/create.go b/api/server/handlers/cluster/create.go index 79df9dbd9c4..fda0eb9e41b 100644 --- a/api/server/handlers/cluster/create.go +++ b/api/server/handlers/cluster/create.go @@ -11,6 +11,7 @@ import ( "github.com/porter-dev/porter/api/server/shared/apierrors" "github.com/porter-dev/porter/api/server/shared/config" "github.com/porter-dev/porter/api/types" + "github.com/porter-dev/porter/internal/features" "github.com/porter-dev/porter/internal/kubernetes/resolver" "github.com/porter-dev/porter/internal/models" "github.com/porter-dev/porter/internal/repository" @@ -46,7 +47,7 @@ func (c *CreateClusterManualHandler) ServeHTTP(w http.ResponseWriter, r *http.Re return } - cluster, err = c.Repo().Cluster().CreateCluster(cluster) + cluster, err = c.Repo().Cluster().CreateCluster(cluster, c.Config().LaunchDarklyClient) if err != nil { c.HandleAPIError(w, r, apierrors.NewErrInternal(err)) @@ -117,6 +118,7 @@ func createClusterFromCandidate( user *models.User, candidate *models.ClusterCandidate, clResolver *types.ClusterResolverAll, + launchDarklyClient *features.Client, ) (*models.Cluster, *models.ClusterCandidate, error) { // we query the repo again to get the decrypted version of the cluster candidate cc, err := repo.Cluster().ReadClusterCandidate(project.ID, candidate.ID) @@ -137,7 +139,7 @@ func createClusterFromCandidate( return nil, nil, err } - cluster, err := cResolver.ResolveCluster(repo) + cluster, err := cResolver.ResolveCluster(repo, launchDarklyClient) if err != nil { return nil, nil, err } diff --git a/api/server/handlers/cluster/create_candidate.go b/api/server/handlers/cluster/create_candidate.go index 8749582db80..e881d2c4d2a 100644 --- a/api/server/handlers/cluster/create_candidate.go +++ b/api/server/handlers/cluster/create_candidate.go @@ -67,7 +67,7 @@ func (c *CreateClusterCandidateHandler) ServeHTTP(w http.ResponseWriter, r *http // automatically if len(cc.Resolvers) == 0 { var cluster *models.Cluster - cluster, cc, err = createClusterFromCandidate(c.Repo(), proj, user, cc, &types.ClusterResolverAll{}) + cluster, cc, err = createClusterFromCandidate(c.Repo(), proj, user, cc, &types.ClusterResolverAll{}, c.Config().LaunchDarklyClient) if err != nil { c.HandleAPIError(w, r, apierrors.NewErrInternal(err)) diff --git a/api/server/handlers/cluster/rename.go b/api/server/handlers/cluster/rename.go index 0e2136d2404..2204a1e647e 100644 --- a/api/server/handlers/cluster/rename.go +++ b/api/server/handlers/cluster/rename.go @@ -40,7 +40,7 @@ func (c *RenameClusterHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) cluster.VanityName = request.Name } - cluster, err := c.Repo().Cluster().UpdateCluster(cluster) + cluster, err := c.Repo().Cluster().UpdateCluster(cluster, c.Config().LaunchDarklyClient) if err != nil { c.HandleAPIError(w, r, apierrors.NewErrInternal(err)) return diff --git a/api/server/handlers/cluster/resolve_candidate.go b/api/server/handlers/cluster/resolve_candidate.go index 071794373d4..c144816522c 100644 --- a/api/server/handlers/cluster/resolve_candidate.go +++ b/api/server/handlers/cluster/resolve_candidate.go @@ -45,7 +45,7 @@ func (c *ResolveClusterCandidateHandler) ServeHTTP(w http.ResponseWriter, r *htt return } - cluster, cc, err := createClusterFromCandidate(c.Repo(), proj, user, cc, request) + cluster, cc, err := createClusterFromCandidate(c.Repo(), proj, user, cc, request, c.Config().LaunchDarklyClient) if err != nil { c.HandleAPIError(w, r, apierrors.NewErrInternal(err)) return diff --git a/api/server/handlers/cluster/update.go b/api/server/handlers/cluster/update.go index 810bf4273e9..ed61c9bf1e7 100644 --- a/api/server/handlers/cluster/update.go +++ b/api/server/handlers/cluster/update.go @@ -72,7 +72,7 @@ func (c *ClusterUpdateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) cluster.Name = request.Name } - cluster, err := c.Repo().Cluster().UpdateCluster(cluster) + cluster, err := c.Repo().Cluster().UpdateCluster(cluster, c.Config().LaunchDarklyClient) if err != nil { c.HandleAPIError(w, r, apierrors.NewErrInternal(err)) return diff --git a/api/server/handlers/gitinstallation/get_porter_yaml.go b/api/server/handlers/gitinstallation/get_porter_yaml.go index 638e07dff52..3be59536da5 100644 --- a/api/server/handlers/gitinstallation/get_porter_yaml.go +++ b/api/server/handlers/gitinstallation/get_porter_yaml.go @@ -102,7 +102,7 @@ func (c *GithubGetPorterYamlHandler) ServeHTTP(w http.ResponseWriter, r *http.Re return } - if project.ValidateApplyV2 { + if project.GetFeatureFlag(models.ValidateApplyV2, c.Config().LaunchDarklyClient) { if parsed.Version != nil && *parsed.Version != "v2" { err = telemetry.Error(ctx, span, nil, "porter YAML version is not supported") c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest)) @@ -111,7 +111,7 @@ func (c *GithubGetPorterYamlHandler) ServeHTTP(w http.ResponseWriter, r *http.Re } // backwards compatibility so that old porter yamls are no longer valid - if !project.ValidateApplyV2 && parsed.Version != nil { + if !project.GetFeatureFlag(models.ValidateApplyV2, c.Config().LaunchDarklyClient) && parsed.Version != nil { version := *parsed.Version if version != "v1stack" { err = telemetry.Error(ctx, span, nil, "porter YAML version is not supported") diff --git a/api/server/handlers/porter_app/apply.go b/api/server/handlers/porter_app/apply.go index a43fcf703bd..41f65df8d4f 100644 --- a/api/server/handlers/porter_app/apply.go +++ b/api/server/handlers/porter_app/apply.go @@ -62,7 +62,7 @@ func (c *ApplyPorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request telemetry.AttributeKV{Key: "cluster-id", Value: cluster.ID}, ) - if !project.ValidateApplyV2 { + if !project.GetFeatureFlag(models.ValidateApplyV2, c.Config().LaunchDarklyClient) { err := telemetry.Error(ctx, span, nil, "project does not have validate apply v2 enabled") c.HandleAPIError(w, r, apierrors.NewErrForbidden(err)) return diff --git a/api/server/handlers/porter_app/create_app.go b/api/server/handlers/porter_app/create_app.go index b49cdd23174..481f31a5728 100644 --- a/api/server/handlers/porter_app/create_app.go +++ b/api/server/handlers/porter_app/create_app.go @@ -97,7 +97,7 @@ func (c *CreateAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { project, _ := ctx.Value(types.ProjectScope).(*models.Project) cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster) - if !project.ValidateApplyV2 { + if !project.GetFeatureFlag(models.ValidateApplyV2, c.Config().LaunchDarklyClient) { err := telemetry.Error(ctx, span, nil, "project does not have validate apply v2 enabled") c.HandleAPIError(w, r, apierrors.NewErrForbidden(err)) return diff --git a/api/server/handlers/porter_app/get.go b/api/server/handlers/porter_app/get.go index 2f0c3317393..d01c94e2ff7 100644 --- a/api/server/handlers/porter_app/get.go +++ b/api/server/handlers/porter_app/get.go @@ -60,7 +60,7 @@ func (c *GetPorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) // this is a temporary fix until we figure out how to reconcile the new revisions table // with dependencies on helm releases throuhg the api - if project.ValidateApplyV2 { + if project.GetFeatureFlag(models.ValidateApplyV2, c.Config().LaunchDarklyClient) { c.WriteResult(w, r, app.ToPorterAppType()) return } diff --git a/api/server/handlers/porter_app/parse_yaml.go b/api/server/handlers/porter_app/parse_yaml.go index d15a01b7f6c..6135d746069 100644 --- a/api/server/handlers/porter_app/parse_yaml.go +++ b/api/server/handlers/porter_app/parse_yaml.go @@ -52,7 +52,7 @@ func (c *ParsePorterYAMLToProtoHandler) ServeHTTP(w http.ResponseWriter, r *http project, _ := ctx.Value(types.ProjectScope).(*models.Project) - if !project.ValidateApplyV2 { + if !project.GetFeatureFlag(models.ValidateApplyV2, c.Config().LaunchDarklyClient) { err := telemetry.Error(ctx, span, nil, "project does not have apply v2 enabled") c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusForbidden)) return diff --git a/api/server/handlers/porter_app/validate.go b/api/server/handlers/porter_app/validate.go index 5b4e28c7d89..c40241c84f0 100644 --- a/api/server/handlers/porter_app/validate.go +++ b/api/server/handlers/porter_app/validate.go @@ -68,7 +68,7 @@ func (c *ValidatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ telemetry.AttributeKV{Key: "cluster-id", Value: cluster.ID}, ) - if !project.ValidateApplyV2 { + if !project.GetFeatureFlag(models.ValidateApplyV2, c.Config().LaunchDarklyClient) { err := telemetry.Error(ctx, span, nil, "project does not have validate apply v2 enabled") c.HandleAPIError(w, r, apierrors.NewErrForbidden(err)) return diff --git a/api/server/handlers/project/delete.go b/api/server/handlers/project/delete.go index a12f903f16b..8f3c0cd6613 100644 --- a/api/server/handlers/project/delete.go +++ b/api/server/handlers/project/delete.go @@ -33,7 +33,7 @@ func (p *ProjectDeleteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) user, _ := ctx.Value(types.UserScope).(*models.User) proj, _ := ctx.Value(types.ProjectScope).(*models.Project) - if proj.CapiProvisionerEnabled { + if proj.GetFeatureFlag(models.CapiProvisionerEnabled, p.Config().LaunchDarklyClient) { clusters, err := p.Config().Repo.Cluster().ListClustersByProjectID(proj.ID) if err != nil { p.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error finding clusters for project: %w", err))) diff --git a/api/server/handlers/project_integration/create_aws.go b/api/server/handlers/project_integration/create_aws.go index b88035c2cc0..483f09e2cb6 100644 --- a/api/server/handlers/project_integration/create_aws.go +++ b/api/server/handlers/project_integration/create_aws.go @@ -56,7 +56,7 @@ func (p *CreateAWSHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { AWSIntegration: aws.ToAWSIntegrationType(), } - if project.CapiProvisionerEnabled && p.Config().EnableCAPIProvisioner { + if project.GetFeatureFlag(models.CapiProvisionerEnabled, p.Config().LaunchDarklyClient) && p.Config().EnableCAPIProvisioner { credReq := porterv1.CreateAssumeRoleChainRequest{ ProjectId: int64(project.ID), SourceArn: "arn:aws:iam::108458755588:role/CAPIManagement", // hard coded as this is the final hop for a CAPI cluster diff --git a/api/server/handlers/project_integration/create_gcp.go b/api/server/handlers/project_integration/create_gcp.go index 11a1df491a6..0b6cda7b748 100644 --- a/api/server/handlers/project_integration/create_gcp.go +++ b/api/server/handlers/project_integration/create_gcp.go @@ -43,7 +43,7 @@ func (p *CreateGCPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - if project.CapiProvisionerEnabled { + if project.GetFeatureFlag(models.CapiProvisionerEnabled, p.Config().LaunchDarklyClient) { telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "capi-provisioner-enabled", Value: true}) b64Key := base64.StdEncoding.EncodeToString([]byte(request.GCPKeyData)) diff --git a/api/server/handlers/project_integration/list_aws.go b/api/server/handlers/project_integration/list_aws.go index b05e1bb9726..37c88fb8ca4 100644 --- a/api/server/handlers/project_integration/list_aws.go +++ b/api/server/handlers/project_integration/list_aws.go @@ -40,7 +40,7 @@ func (p *ListAWSHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { ctx := r.Context() project, _ := ctx.Value(types.ProjectScope).(*models.Project) - if project.CapiProvisionerEnabled { + if project.GetFeatureFlag(models.CapiProvisionerEnabled, p.Config().LaunchDarklyClient) { dblinks, err := p.Repo().AWSAssumeRoleChainer().List(ctx, project.ID) if err != nil { e := fmt.Errorf("unable to find assume role chain links: %w", err) diff --git a/api/server/handlers/registry/get_token.go b/api/server/handlers/registry/get_token.go index fc47eb6aab5..3af633c5bb8 100644 --- a/api/server/handlers/registry/get_token.go +++ b/api/server/handlers/registry/get_token.go @@ -47,7 +47,7 @@ func (c *RegistryGetECRTokenHandler) ServeHTTP(w http.ResponseWriter, r *http.Re return } - if proj.CapiProvisionerEnabled { + if proj.GetFeatureFlag(models.CapiProvisionerEnabled, c.Config().LaunchDarklyClient) { ecrRequest := porterv1.ECRTokenForRegistryRequest{ ProjectId: int64(proj.ID), Region: request.Region, @@ -247,7 +247,7 @@ func (c *RegistryGetGARTokenHandler) ServeHTTP(w http.ResponseWriter, r *http.Re return } - if proj.CapiProvisionerEnabled { + if proj.GetFeatureFlag(models.CapiProvisionerEnabled, c.Config().LaunchDarklyClient) { regInput := connect.NewRequest(&porterv1.TokenForRegistryRequest{ ProjectId: int64(proj.ID), RegistryUri: regs[0].URL, @@ -493,7 +493,7 @@ func (c *RegistryGetACRTokenHandler) ServeHTTP(w http.ResponseWriter, r *http.Re telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "registry-name", Value: matchingReg.Name}) - if proj.CapiProvisionerEnabled { + if proj.GetFeatureFlag(models.CapiProvisionerEnabled, c.Config().LaunchDarklyClient) { telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "capi-provisioned", Value: true}) if c.Config().ClusterControlPlaneClient == nil { diff --git a/api/server/shared/config/loader/loader.go b/api/server/shared/config/loader/loader.go index 8a77a9c43ab..9eab8bb5924 100644 --- a/api/server/shared/config/loader/loader.go +++ b/api/server/shared/config/loader/loader.go @@ -243,7 +243,7 @@ func (e *EnvConfigLoader) LoadConfig() (res *config.Config, err error) { sc.GithubAppSecret = append(sc.GithubAppSecret, secret...) } - launchDarklyClient, err := features.GetClient(envConf) + launchDarklyClient, err := features.GetClient(envConf.ServerConf.LaunchDarklySDKKey) if err != nil { return nil, fmt.Errorf("could not create launch darkly client: %s", err) } diff --git a/cmd/app/main.go b/cmd/app/main.go index 13b6c25d9c7..d243a260195 100644 --- a/cmd/app/main.go +++ b/cmd/app/main.go @@ -181,7 +181,7 @@ func initData(conf *config.Config) error { AuthMechanism: models.InCluster, ProjectID: 1, MonitorHelmReleases: true, - }) + }, conf.LaunchDarklyClient) if err != nil { return err diff --git a/cmd/migrate/enable_cluster_preview_envs/enable.go b/cmd/migrate/enable_cluster_preview_envs/enable.go index 38811bd46f7..70d74221ebf 100644 --- a/cmd/migrate/enable_cluster_preview_envs/enable.go +++ b/cmd/migrate/enable_cluster_preview_envs/enable.go @@ -1,12 +1,14 @@ package enable_cluster_preview_envs import ( + "github.com/porter-dev/porter/internal/features" "github.com/porter-dev/porter/internal/models" lr "github.com/porter-dev/porter/pkg/logger" _gorm "gorm.io/gorm" ) -func EnableClusterPreviewEnvs(db *_gorm.DB, logger *lr.Logger) error { +// EnableClusterPreviewEnvs enables preview environments for clusters where it is enabled for the project +func EnableClusterPreviewEnvs(db *_gorm.DB, client *features.Client, logger *lr.Logger) error { logger.Info().Msg("starting to enable preview envs for existing clusters whose parent projects have preview envs enabled") var clusters []*models.Cluster @@ -24,7 +26,7 @@ func EnableClusterPreviewEnvs(db *_gorm.DB, logger *lr.Logger) error { continue } - if project.PreviewEnvsEnabled { + if project.GetFeatureFlag(models.PreviewEnvsEnabled, client) { c.PreviewEnvsEnabled = true if err := db.Save(c).Error; err != nil { diff --git a/cmd/migrate/enable_cluster_preview_envs/enable_test.go b/cmd/migrate/enable_cluster_preview_envs/enable_test.go index 39e100aefc6..d1ab43a26e4 100644 --- a/cmd/migrate/enable_cluster_preview_envs/enable_test.go +++ b/cmd/migrate/enable_cluster_preview_envs/enable_test.go @@ -3,9 +3,19 @@ package enable_cluster_preview_envs import ( "testing" + "github.com/launchdarkly/go-sdk-common/v3/ldcontext" + "github.com/porter-dev/porter/internal/features" lr "github.com/porter-dev/porter/pkg/logger" ) +type FeaturesTestClient struct { + override bool +} + +func (c FeaturesTestClient) BoolVariation(key string, context ldcontext.Context, defaultVal bool) (bool, error) { + return c.override, nil +} + func TestEnableForProjectEnabled(t *testing.T) { logger := lr.NewConsole(true) @@ -20,7 +30,7 @@ func TestEnableForProjectEnabled(t *testing.T) { initProjectPreviewEnabled(tester, t) initCluster(tester, t) - err := EnableClusterPreviewEnvs(tester.DB, logger) + err := EnableClusterPreviewEnvs(tester.DB, &features.Client{Client: FeaturesTestClient{true}}, logger) if err != nil { t.Fatalf("%v\n", err) return @@ -51,7 +61,7 @@ func TestEnableForProjectDisabled(t *testing.T) { initProjectPreviewDisabled(tester, t) initCluster(tester, t) - err := EnableClusterPreviewEnvs(tester.DB, logger) + err := EnableClusterPreviewEnvs(tester.DB, &features.Client{Client: FeaturesTestClient{false}}, logger) if err != nil { t.Fatalf("%v\n", err) return diff --git a/cmd/migrate/enable_cluster_preview_envs/helpers_test.go b/cmd/migrate/enable_cluster_preview_envs/helpers_test.go index 27102b549fa..de9cea36264 100644 --- a/cmd/migrate/enable_cluster_preview_envs/helpers_test.go +++ b/cmd/migrate/enable_cluster_preview_envs/helpers_test.go @@ -7,6 +7,7 @@ import ( "github.com/porter-dev/porter/api/server/shared/config/env" "github.com/porter-dev/porter/internal/adapter" + "github.com/porter-dev/porter/internal/features" "github.com/porter-dev/porter/internal/models" ints "github.com/porter-dev/porter/internal/models/integrations" "github.com/porter-dev/porter/internal/repository" @@ -109,7 +110,7 @@ func initCluster(tester *tester, t *testing.T) { }, } - cluster, err := tester.repo.Cluster().CreateCluster(cluster) + cluster, err := tester.repo.Cluster().CreateCluster(cluster, &features.Client{}) if err != nil { t.Fatalf("%v\n", err) } diff --git a/cmd/migrate/keyrotate/helpers_test.go b/cmd/migrate/keyrotate/helpers_test.go index 6f91b542763..6e439874796 100644 --- a/cmd/migrate/keyrotate/helpers_test.go +++ b/cmd/migrate/keyrotate/helpers_test.go @@ -8,6 +8,7 @@ import ( "github.com/porter-dev/porter/api/server/shared/config/env" "github.com/porter-dev/porter/api/types" "github.com/porter-dev/porter/internal/adapter" + "github.com/porter-dev/porter/internal/features" "github.com/porter-dev/porter/internal/models" ints "github.com/porter-dev/porter/internal/models/integrations" "github.com/porter-dev/porter/internal/repository" @@ -377,7 +378,7 @@ func initCluster(tester *tester, t *testing.T) { }, } - cluster, err := tester.repo.Cluster().CreateCluster(cluster) + cluster, err := tester.repo.Cluster().CreateCluster(cluster, &features.Client{}) if err != nil { t.Fatalf("%v\n", err) } diff --git a/cmd/migrate/main.go b/cmd/migrate/main.go index bd3516a772b..3f189ed084f 100644 --- a/cmd/migrate/main.go +++ b/cmd/migrate/main.go @@ -11,6 +11,7 @@ import ( "github.com/porter-dev/porter/cmd/migrate/startup_migrations" adapter "github.com/porter-dev/porter/internal/adapter" + "github.com/porter-dev/porter/internal/features" "github.com/porter-dev/porter/internal/models" "github.com/porter-dev/porter/internal/repository/gorm" lr "github.com/porter-dev/porter/pkg/logger" @@ -29,6 +30,12 @@ func main() { return } + launchDarklyClient, err := features.GetClient(envConf.ServerConf.LaunchDarklySDKKey) + if err != nil { + logger.Fatal().Err(err).Msg("could not load launch darkly client") + return + } + db, err := adapter.New(envConf.DBConf) if err != nil { logger.Fatal().Err(err).Msg("could not connect to the database") @@ -103,7 +110,7 @@ func main() { if dbMigration.Version < latestMigrationVersion { for ver, fn := range startup_migrations.StartupMigrations { if ver > dbMigration.Version { - err := fn(tx, logger) + err := fn(tx, launchDarklyClient, logger) if err != nil { tx.Rollback() diff --git a/cmd/migrate/populate_source_config_display_name/helpers_test.go b/cmd/migrate/populate_source_config_display_name/helpers_test.go index 4247c256c79..4d60d07ed97 100644 --- a/cmd/migrate/populate_source_config_display_name/helpers_test.go +++ b/cmd/migrate/populate_source_config_display_name/helpers_test.go @@ -10,6 +10,7 @@ import ( "github.com/porter-dev/porter/api/types" "github.com/porter-dev/porter/internal/adapter" "github.com/porter-dev/porter/internal/encryption" + "github.com/porter-dev/porter/internal/features" "github.com/porter-dev/porter/internal/models" ints "github.com/porter-dev/porter/internal/models/integrations" "github.com/porter-dev/porter/internal/repository" @@ -121,7 +122,7 @@ func initCluster(tester *tester, t *testing.T) { }, } - cluster, err := tester.repo.Cluster().CreateCluster(cluster) + cluster, err := tester.repo.Cluster().CreateCluster(cluster, &features.Client{}) if err != nil { t.Fatalf("%v\n", err) } diff --git a/cmd/migrate/startup_migrations/global_map.go b/cmd/migrate/startup_migrations/global_map.go index af5662ece04..491fd53591e 100644 --- a/cmd/migrate/startup_migrations/global_map.go +++ b/cmd/migrate/startup_migrations/global_map.go @@ -2,6 +2,7 @@ package startup_migrations import ( "github.com/porter-dev/porter/cmd/migrate/enable_cluster_preview_envs" + "github.com/porter-dev/porter/internal/features" lr "github.com/porter-dev/porter/pkg/logger" "gorm.io/gorm" ) @@ -9,7 +10,7 @@ import ( // this should be incremented with every new startup migration script const LatestMigrationVersion uint = 1 -type migrationFunc func(db *gorm.DB, logger *lr.Logger) error +type migrationFunc func(db *gorm.DB, config *features.Client, logger *lr.Logger) error var StartupMigrations = make(map[uint]migrationFunc) diff --git a/internal/features/launch_darkly.go b/internal/features/launch_darkly.go index d1baa742da4..2dbc59089cf 100644 --- a/internal/features/launch_darkly.go +++ b/internal/features/launch_darkly.go @@ -7,12 +7,17 @@ import ( "github.com/launchdarkly/go-sdk-common/v3/ldcontext" ld "github.com/launchdarkly/go-server-sdk/v6" - "github.com/porter-dev/porter/api/server/shared/config/envloader" ) // Client is a struct wrapper around the launchdarkly client type Client struct { - client *ld.LDClient + Client LDClient +} + +// LDClient is an interface that allows us to mock +// the LaunchDarkly client in tests +type LDClient interface { + BoolVariation(key string, context ldcontext.Context, defaultVal bool) (bool, error) } // BoolVariation returns the value of a boolean feature flag for a given evaluation context. @@ -22,15 +27,15 @@ type Client struct { // // For more information, see the Reference Guide: https://docs.launchdarkly.com/sdk/features/evaluating#go func (c Client) BoolVariation(field string, context ldcontext.Context, defaultValue bool) (bool, error) { - if c.client == nil { + if c.Client == nil { return defaultValue, errors.New("failed to participate in launchdarkly test: no client available") } - return c.client.BoolVariation(field, context, defaultValue) + return c.Client.BoolVariation(field, context, defaultValue) } // GetClient retrieves a Client for interacting with LaunchDarkly -func GetClient(envConf *envloader.EnvConf) (*Client, error) { - ldClient, err := ld.MakeClient(envConf.ServerConf.LaunchDarklySDKKey, 5*time.Second) +func GetClient(launchDarklySDKKey string) (*Client, error) { + ldClient, err := ld.MakeClient(launchDarklySDKKey, 5*time.Second) if err != nil { return &Client{}, fmt.Errorf("failed to create new launchdarkly client: %w", err) } diff --git a/internal/kubernetes/resolver/resolver.go b/internal/kubernetes/resolver/resolver.go index ab111d64438..c3545f492f4 100644 --- a/internal/kubernetes/resolver/resolver.go +++ b/internal/kubernetes/resolver/resolver.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/porter-dev/porter/api/types" + "github.com/porter-dev/porter/internal/features" "github.com/porter-dev/porter/internal/kubernetes" "github.com/porter-dev/porter/internal/models" "github.com/porter-dev/porter/internal/repository" @@ -344,6 +345,7 @@ func (rcf *CandidateResolver) resolveAWS( // rcf.ResolveIntegration, since it relies on the previously created integration. func (rcf *CandidateResolver) ResolveCluster( repo repository.Repository, + launchDarklyClient *features.Client, ) (*models.Cluster, error) { // build a cluster from the candidate cluster, err := rcf.buildCluster() @@ -352,7 +354,7 @@ func (rcf *CandidateResolver) ResolveCluster( } // save cluster to db - return repo.Cluster().CreateCluster(cluster) + return repo.Cluster().CreateCluster(cluster, launchDarklyClient) } func (rcf *CandidateResolver) buildCluster() (*models.Cluster, error) { diff --git a/internal/models/project.go b/internal/models/project.go index 6697e21d2a2..334c7bbe3bd 100644 --- a/internal/models/project.go +++ b/internal/models/project.go @@ -11,21 +11,66 @@ import ( ints "github.com/porter-dev/porter/internal/models/integrations" ) +// FeatureFlagLabel strongly types project feature flags +type FeatureFlagLabel string + +const ( + // APITokensEnabled allows users to create Bearer tokens for use with the Porter API + // #nosec G101 - Not actually an api token + APITokensEnabled FeatureFlagLabel = "api_tokens_enabled" + + // AzureEnabled enables Azure Provisioning + AzureEnabled FeatureFlagLabel = "azure_enabled" + + // CapiProvisionerEnabled enables the CAPI Provisioning flow + CapiProvisionerEnabled FeatureFlagLabel = "capi_provisioner_enabled" + + // EnableReprovision enables the provisioning button after initial creation of the cluster + EnableReprovision FeatureFlagLabel = "enable_reprovision" + + // FullAddOns shows all addons, not just curated + FullAddOns FeatureFlagLabel = "full_add_ons" + + // HelmValuesEnabled shows the helm values tab for porter apps (when simplified_view_enabled=true) + HelmValuesEnabled FeatureFlagLabel = "helm_values_enabled" + + // ManagedInfraEnabled uses terraform provisioning instead of capi + ManagedInfraEnabled FeatureFlagLabel = "managed_infra_enabled" + + // MultiCluster allows multiple clusters in simplified view (simplified_view_enabled=true) + MultiCluster FeatureFlagLabel = "multi_cluster" + + // PreviewEnvsEnabled allows legacy user the ability to see preview environments in sidebar (simplified_view_enabled=false) + PreviewEnvsEnabled FeatureFlagLabel = "preview_envs_enabled" + + // RDSDatabasesEnabled allows for users to provision RDS instances within their cluster vpc + RDSDatabasesEnabled FeatureFlagLabel = "rds_databases_enabled" + + // SimplifiedViewEnabled shows the new UI dashboard or not + SimplifiedViewEnabled FeatureFlagLabel = "simplified_view_enabled" + + // StacksEnabled uses stack view for legacy (simplified_view_enabled=false) + StacksEnabled FeatureFlagLabel = "stacks_enabled" + + // ValidateApplyV2 controls whether apps deploys use a porter app revision contract vs helm + ValidateApplyV2 FeatureFlagLabel = "validate_apply_v2" +) + // ProjectFeatureFlags keeps track of all project-related feature flags -var ProjectFeatureFlags = map[string]bool{ - "api_tokens_enabled": false, - "azure_enabled": false, - "capi_provisioner_enabled": true, - "enable_reprovision": false, - "full_add_ons": false, - "helm_values_enabled": false, - "managed_infra_enabled": false, - "multi_cluster": false, - "preview_envs_enabled": false, - "rds_databases_enabled": false, - "simplified_view_enabled": true, - "stacks_enabled": false, - "validate_apply_v2": false, +var ProjectFeatureFlags = map[FeatureFlagLabel]bool{ + APITokensEnabled: false, + AzureEnabled: false, + CapiProvisionerEnabled: true, + EnableReprovision: false, + FullAddOns: false, + HelmValuesEnabled: false, + ManagedInfraEnabled: false, + MultiCluster: false, + PreviewEnvsEnabled: false, + RDSDatabasesEnabled: false, + SimplifiedViewEnabled: true, + StacksEnabled: false, + ValidateApplyV2: false, } type ProjectPlan string @@ -79,30 +124,56 @@ type Project struct { AzureIntegrations []ints.AzureIntegration `json:"azure_integrations"` GitlabIntegrations []ints.GitlabIntegration `json:"gitlab_integrations"` - PreviewEnvsEnabled bool - RDSDatabasesEnabled bool - ManagedInfraEnabled bool - StacksEnabled bool - APITokensEnabled bool + // Deprecated: use p.GetFeatureFlag(PreviewEnvsEnabled, *features.Client) instead + PreviewEnvsEnabled bool + + // Deprecated: use p.GetFeatureFlag(RDSDatabasesEnabled, *features.Client) instead + + RDSDatabasesEnabled bool + // Deprecated: use p.GetFeatureFlag(ManagedInfraEnabled, *features.Client) instead + + ManagedInfraEnabled bool + // Deprecated: use p.GetFeatureFlag(StacksEnabled, *features.Client) instead + + StacksEnabled bool + // Deprecated: use p.GetFeatureFlag(APITokensEnabled, *features.Client) instead + + APITokensEnabled bool + // Deprecated: use p.GetFeatureFlag(CapiProvisionerEnabled, *features.Client) instead + CapiProvisionerEnabled bool - SimplifiedViewEnabled bool - AzureEnabled bool - HelmValuesEnabled bool - MultiCluster bool `gorm:"default:false"` - FullAddOns bool `gorm:"default:false"` - ValidateApplyV2 bool `gorm:"default:false"` - EnableReprovision bool `gorm:"default:false"` + // Deprecated: use p.GetFeatureFlag(SimplifiedViewEnabled, *features.Client) instead + + SimplifiedViewEnabled bool + // Deprecated: use p.GetFeatureFlag(AzureEnabled, *features.Client) instead + + AzureEnabled bool + // Deprecated: use p.GetFeatureFlag(HelmValuesEnabled, *features.Client) instead + + HelmValuesEnabled bool + // Deprecated: use p.GetFeatureFlag(MultiCluster, *features.Client) instead + + MultiCluster bool `gorm:"default:false"` + // Deprecated: use p.GetFeatureFlag(FullAddOns, *features.Client) instead + + FullAddOns bool `gorm:"default:false"` + // Deprecated: use p.GetFeatureFlag(ValidateApplyV2, *features.Client) instead + + ValidateApplyV2 bool `gorm:"default:false"` + // Deprecated: use p.GetFeatureFlag(EnableReprovision, *features.Client) instead + + EnableReprovision bool `gorm:"default:false"` } // GetFeatureFlag calls launchdarkly for the specified flag // and returns the configured value -func (p *Project) GetFeatureFlag(flagName string, launchDarklyClient *features.Client) bool { +func (p *Project) GetFeatureFlag(flagName FeatureFlagLabel, launchDarklyClient *features.Client) bool { projectID := p.ID projectName := p.Name ldContext := getProjectContext(projectID, projectName) defaultValue := ProjectFeatureFlags[flagName] - value, _ := launchDarklyClient.BoolVariation(flagName, ldContext, defaultValue) + value, _ := launchDarklyClient.BoolVariation(string(flagName), ldContext, defaultValue) return value } @@ -122,19 +193,19 @@ func (p *Project) ToProjectType(launchDarklyClient *features.Client) types.Proje Name: projectName, Roles: roles, - PreviewEnvsEnabled: p.GetFeatureFlag("preview_envs_enabled", launchDarklyClient), - RDSDatabasesEnabled: p.GetFeatureFlag("rds_databases_enabled", launchDarklyClient), - ManagedInfraEnabled: p.GetFeatureFlag("managed_infra_enabled", launchDarklyClient), - StacksEnabled: p.GetFeatureFlag("stacks_enabled", launchDarklyClient), - APITokensEnabled: p.GetFeatureFlag("api_tokens_enabled", launchDarklyClient), - CapiProvisionerEnabled: p.GetFeatureFlag("capi_provisioner_enabled", launchDarklyClient), - SimplifiedViewEnabled: p.GetFeatureFlag("simplified_view_enabled", launchDarklyClient), - AzureEnabled: p.GetFeatureFlag("azure_enabled", launchDarklyClient), - HelmValuesEnabled: p.GetFeatureFlag("helm_values_enabled", launchDarklyClient), - MultiCluster: p.GetFeatureFlag("multi_cluster", launchDarklyClient), - EnableReprovision: p.GetFeatureFlag("enable_reprovision", launchDarklyClient), - ValidateApplyV2: p.GetFeatureFlag("validate_apply_v2", launchDarklyClient), - FullAddOns: p.GetFeatureFlag("full_add_ons", launchDarklyClient), + PreviewEnvsEnabled: p.GetFeatureFlag(PreviewEnvsEnabled, launchDarklyClient), + RDSDatabasesEnabled: p.GetFeatureFlag(RDSDatabasesEnabled, launchDarklyClient), + ManagedInfraEnabled: p.GetFeatureFlag(ManagedInfraEnabled, launchDarklyClient), + StacksEnabled: p.GetFeatureFlag(StacksEnabled, launchDarklyClient), + APITokensEnabled: p.GetFeatureFlag(APITokensEnabled, launchDarklyClient), + CapiProvisionerEnabled: p.GetFeatureFlag(CapiProvisionerEnabled, launchDarklyClient), + SimplifiedViewEnabled: p.GetFeatureFlag(SimplifiedViewEnabled, launchDarklyClient), + AzureEnabled: p.GetFeatureFlag(AzureEnabled, launchDarklyClient), + HelmValuesEnabled: p.GetFeatureFlag(HelmValuesEnabled, launchDarklyClient), + MultiCluster: p.GetFeatureFlag(MultiCluster, launchDarklyClient), + EnableReprovision: p.GetFeatureFlag(EnableReprovision, launchDarklyClient), + ValidateApplyV2: p.GetFeatureFlag(ValidateApplyV2, launchDarklyClient), + FullAddOns: p.GetFeatureFlag(FullAddOns, launchDarklyClient), } } diff --git a/internal/registry/registry.go b/internal/registry/registry.go index 59cb5e6d17b..f35f55407dd 100644 --- a/internal/registry/registry.go +++ b/internal/registry/registry.go @@ -154,7 +154,7 @@ func (r *Registry) ListRepositories( return nil, telemetry.Error(ctx, span, err, "error getting project for repository") } - if project.CapiProvisionerEnabled { + if project.GetFeatureFlag(models.CapiProvisionerEnabled, conf.LaunchDarklyClient) { // TODO: Remove this conditional when AWS list repos is supported in CCP telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "registry-uri", Value: r.URL}) @@ -899,7 +899,7 @@ func (r *Registry) CreateRepository( return telemetry.Error(ctx, span, err, "error getting project for repository") } - if project.CapiProvisionerEnabled { + if project.GetFeatureFlag(models.CapiProvisionerEnabled, conf.LaunchDarklyClient) { // no need to create repository if pushing to ACR or GAR if strings.Contains(r.URL, ".azurecr.") || strings.Contains(r.URL, "-docker.pkg.dev") { telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "skipping-create-repo", Value: true}) @@ -1063,7 +1063,7 @@ func (r *Registry) ListImages( return nil, fmt.Errorf("error getting project for repository: %w", err) } - if project.CapiProvisionerEnabled { + if project.GetFeatureFlag(models.CapiProvisionerEnabled, conf.LaunchDarklyClient) { uri := strings.TrimPrefix(r.URL, "https://") splits := strings.Split(uri, ".") accountID := splits[0] diff --git a/internal/repository/cluster.go b/internal/repository/cluster.go index fa1aa0932c2..1a7a316e36a 100644 --- a/internal/repository/cluster.go +++ b/internal/repository/cluster.go @@ -1,6 +1,7 @@ package repository import ( + "github.com/porter-dev/porter/internal/features" "github.com/porter-dev/porter/internal/models" ints "github.com/porter-dev/porter/internal/models/integrations" ) @@ -13,11 +14,11 @@ type ClusterRepository interface { ListClusterCandidatesByProjectID(projectID uint) ([]*models.ClusterCandidate, error) UpdateClusterCandidateCreatedClusterID(id uint, createdClusterID uint) (*models.ClusterCandidate, error) - CreateCluster(cluster *models.Cluster) (*models.Cluster, error) + CreateCluster(cluster *models.Cluster, launchDarklyClient *features.Client) (*models.Cluster, error) ReadCluster(projectID, clusterID uint) (*models.Cluster, error) ReadClusterByInfraID(projectID, infraID uint) (*models.Cluster, error) ListClustersByProjectID(projectID uint) ([]*models.Cluster, error) - UpdateCluster(cluster *models.Cluster) (*models.Cluster, error) + UpdateCluster(cluster *models.Cluster, launchDarklyClient *features.Client) (*models.Cluster, error) UpdateClusterTokenCache(tokenCache *ints.ClusterTokenCache) (*models.Cluster, error) DeleteCluster(cluster *models.Cluster) error } diff --git a/internal/repository/gorm/cluster.go b/internal/repository/gorm/cluster.go index f615d36156e..f56bc83986b 100644 --- a/internal/repository/gorm/cluster.go +++ b/internal/repository/gorm/cluster.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/porter-dev/porter/internal/encryption" + "github.com/porter-dev/porter/internal/features" "github.com/porter-dev/porter/internal/models" "github.com/porter-dev/porter/internal/repository" "gorm.io/gorm" @@ -118,6 +119,7 @@ func (repo *ClusterRepository) UpdateClusterCandidateCreatedClusterID( // CreateCluster creates a new cluster func (repo *ClusterRepository) CreateCluster( cluster *models.Cluster, + launchDarkluClient *features.Client, ) (*models.Cluster, error) { err := repo.EncryptClusterData(cluster, repo.key) if err != nil { @@ -130,7 +132,7 @@ func (repo *ClusterRepository) CreateCluster( return nil, err } - if cluster.PreviewEnvsEnabled && !project.PreviewEnvsEnabled { + if cluster.PreviewEnvsEnabled && !project.GetFeatureFlag(models.PreviewEnvsEnabled, launchDarkluClient) { // this should only work if the corresponding project has preview environments enabled cluster.PreviewEnvsEnabled = false } @@ -246,6 +248,7 @@ func (repo *ClusterRepository) ListClustersByProjectID( // UpdateCluster modifies an existing Cluster in the database func (repo *ClusterRepository) UpdateCluster( cluster *models.Cluster, + launchDarklyClient *features.Client, ) (*models.Cluster, error) { err := repo.EncryptClusterData(cluster, repo.key) if err != nil { @@ -260,7 +263,7 @@ func (repo *ClusterRepository) UpdateCluster( return nil, fmt.Errorf("error fetching details about cluster's project: %w", err) } - if !project.PreviewEnvsEnabled { + if !project.GetFeatureFlag(models.PreviewEnvsEnabled, launchDarklyClient) { cluster.PreviewEnvsEnabled = false } } diff --git a/internal/repository/gorm/cluster_test.go b/internal/repository/gorm/cluster_test.go index ccac5320e2a..2027da45fff 100644 --- a/internal/repository/gorm/cluster_test.go +++ b/internal/repository/gorm/cluster_test.go @@ -6,6 +6,7 @@ import ( "github.com/go-test/deep" "github.com/porter-dev/porter/api/types" + "github.com/porter-dev/porter/internal/features" "github.com/porter-dev/porter/internal/models" ints "github.com/porter-dev/porter/internal/models/integrations" orm "gorm.io/gorm" @@ -228,7 +229,7 @@ func TestCreateCluster(t *testing.T) { expCluster := *cluster - cluster, err := tester.repo.Cluster().CreateCluster(cluster) + cluster, err := tester.repo.Cluster().CreateCluster(cluster, &features.Client{}) if err != nil { t.Fatalf("%v\n", err) } @@ -315,6 +316,7 @@ func TestUpdateCluster(t *testing.T) { cluster, err := tester.repo.Cluster().UpdateCluster( cluster, + &features.Client{}, ) if err != nil { t.Fatalf("%v\n", err) @@ -369,7 +371,7 @@ func TestUpdateClusterToken(t *testing.T) { }, } - cluster, err := tester.repo.Cluster().CreateCluster(cluster) + cluster, err := tester.repo.Cluster().CreateCluster(cluster, &features.Client{}) if err != nil { t.Fatalf("%v\n", err) } diff --git a/internal/repository/gorm/helpers_test.go b/internal/repository/gorm/helpers_test.go index 8dbd2aebe60..e7859dc6527 100644 --- a/internal/repository/gorm/helpers_test.go +++ b/internal/repository/gorm/helpers_test.go @@ -9,6 +9,7 @@ import ( "github.com/porter-dev/porter/api/server/shared/config/env" "github.com/porter-dev/porter/api/types" "github.com/porter-dev/porter/internal/adapter" + "github.com/porter-dev/porter/internal/features" "github.com/porter-dev/porter/internal/models" ints "github.com/porter-dev/porter/internal/models/integrations" "github.com/porter-dev/porter/internal/repository" @@ -416,7 +417,7 @@ func initCluster(tester *tester, t *testing.T) { CertificateAuthorityData: []byte("-----BEGIN"), } - cluster, err := tester.repo.Cluster().CreateCluster(cluster) + cluster, err := tester.repo.Cluster().CreateCluster(cluster, &features.Client{}) if err != nil { t.Fatalf("%v\n", err) } diff --git a/internal/repository/test/cluster.go b/internal/repository/test/cluster.go index 1839534c0cb..e96e14acd44 100644 --- a/internal/repository/test/cluster.go +++ b/internal/repository/test/cluster.go @@ -3,6 +3,7 @@ package test import ( "errors" + "github.com/porter-dev/porter/internal/features" "github.com/porter-dev/porter/internal/models" "github.com/porter-dev/porter/internal/repository" "gorm.io/gorm" @@ -93,6 +94,7 @@ func (repo *ClusterRepository) UpdateClusterCandidateCreatedClusterID( // CreateCluster creates a new servicea account func (repo *ClusterRepository) CreateCluster( cluster *models.Cluster, + launchDarklyClient *features.Client, ) (*models.Cluster, error) { if !repo.canQuery { return nil, errors.New("Cannot write database") @@ -153,6 +155,7 @@ func (repo *ClusterRepository) ListClustersByProjectID( // UpdateCluster modifies an existing Cluster in the database func (repo *ClusterRepository) UpdateCluster( cluster *models.Cluster, + launchDarklyClient *features.Client, ) (*models.Cluster, error) { if !repo.canQuery { return nil, errors.New("Cannot write database") diff --git a/provisioner/server/config/config.go b/provisioner/server/config/config.go index 79686909601..6a79fd24b05 100644 --- a/provisioner/server/config/config.go +++ b/provisioner/server/config/config.go @@ -13,6 +13,7 @@ import ( "github.com/porter-dev/porter/api/server/shared/config/env" "github.com/porter-dev/porter/internal/adapter" "github.com/porter-dev/porter/internal/analytics" + "github.com/porter-dev/porter/internal/features" "github.com/porter-dev/porter/internal/kubernetes" klocal "github.com/porter-dev/porter/internal/kubernetes/local" "github.com/porter-dev/porter/internal/oauth" @@ -69,6 +70,9 @@ type Config struct { // DOConf is the configuration for a DigitalOcean OAuth client DOConf *oauth2.Config + // LaunchDarklyClient is the client for the LaunchDarkly feature flag service + LaunchDarklyClient *features.Client + RedisClient *redis.Client Provisioner provisioner.Provisioner @@ -121,6 +125,9 @@ type ProvisionerConf struct { // Client key for segment to report provisioning events SegmentClientKey string `env:"SEGMENT_CLIENT_KEY"` + + // Launch Darkly SDK key + LaunchDarklySDKKey string `env:"LAUNCHDARKLY_SDK_KEY"` } type EnvConf struct { @@ -165,6 +172,12 @@ func GetConfig(envConf *EnvConf) (*Config, error) { res.Repo = gorm.NewRepository(db, &key, InstanceCredentialBackend) + launchDarklyClient, err := features.GetClient(envConf.LaunchDarklySDKKey) + if err != nil { + return nil, fmt.Errorf("could not create launch darkly client: %s", err) + } + res.LaunchDarklyClient = launchDarklyClient + if envConf.ProvisionerConf.SentryDSN != "" { res.Alerter, err = alerter.NewSentryAlerter(envConf.ProvisionerConf.SentryDSN, envConf.ProvisionerConf.SentryEnv) } diff --git a/provisioner/server/handlers/state/create_resource.go b/provisioner/server/handlers/state/create_resource.go index 9fc0ed7d86b..6ac3fdebe00 100644 --- a/provisioner/server/handlers/state/create_resource.go +++ b/provisioner/server/handlers/state/create_resource.go @@ -15,6 +15,7 @@ import ( "github.com/porter-dev/porter/api/server/shared/apierrors" "github.com/porter-dev/porter/api/types" "github.com/porter-dev/porter/internal/analytics" + "github.com/porter-dev/porter/internal/features" "github.com/porter-dev/porter/internal/kubernetes" "github.com/porter-dev/porter/internal/kubernetes/envgroup" "github.com/porter-dev/porter/internal/models" @@ -79,7 +80,7 @@ func (c *CreateResourceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request switch req.Kind { case string(types.InfraEKS), string(types.InfraDOKS), string(types.InfraGKE), string(types.InfraAKS): var cluster *models.Cluster - cluster, err = createCluster(c.Config, infra, operation, req.Output) + cluster, err = createCluster(c.Config, infra, c.Config.LaunchDarklyClient, req.Output) if cluster != nil { c.Config.AnalyticsClient.Track(analytics.ClusterProvisioningSuccessTrack( &analytics.ClusterProvisioningSuccessTrackOpts{ @@ -197,7 +198,7 @@ func createS3Bucket(ctx context.Context, config *config.Config, infra *models.In return createS3EnvGroup(ctx, config, infra, lastApplied, output) } -func createCluster(config *config.Config, infra *models.Infra, operation *models.Operation, output map[string]interface{}) (*models.Cluster, error) { +func createCluster(config *config.Config, infra *models.Infra, launchDarklyClient *features.Client, output map[string]interface{}) (*models.Cluster, error) { // check for infra id being 0 as a safeguard so that all non-provisioned // clusters are not matched by read if infra.ID == 0 { @@ -239,9 +240,9 @@ func createCluster(config *config.Config, infra *models.Infra, operation *models cluster.Server = output["cluster_endpoint"].(string) cluster.CertificateAuthorityData = caData if isNotFound { - cluster, err = config.Repo.Cluster().CreateCluster(cluster) + cluster, err = config.Repo.Cluster().CreateCluster(cluster, launchDarklyClient) } else { - cluster, err = config.Repo.Cluster().UpdateCluster(cluster) + cluster, err = config.Repo.Cluster().UpdateCluster(cluster, launchDarklyClient) } if err != nil { return nil, err From ccaf91377f1ef069411d4f95d4b7caf5e9533db0 Mon Sep 17 00:00:00 2001 From: Feroze Mohideen Date: Tue, 12 Sep 2023 15:10:16 -0400 Subject: [PATCH 15/59] Pass through more metadata for build and pre-deploy events (#3552) --- cli/cmd/v2/app_events.go | 3 ++- cli/cmd/v2/apply.go | 9 ++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/cli/cmd/v2/app_events.go b/cli/cmd/v2/app_events.go index f2aa38b0393..4b3c3cadd27 100644 --- a/cli/cmd/v2/app_events.go +++ b/cli/cmd/v2/app_events.go @@ -62,7 +62,7 @@ func createBuildEvent(ctx context.Context, client api.Client, applicationName st return event.ID, nil } -func createPredeployEvent(ctx context.Context, client api.Client, applicationName string, projectId, clusterId uint, deploymentTargetID string, createdAt time.Time) (string, error) { +func createPredeployEvent(ctx context.Context, client api.Client, applicationName string, projectId, clusterId uint, deploymentTargetID string, createdAt time.Time, appRevisionID string) (string, error) { ctx, span := telemetry.NewSpan(ctx, "create-predeploy-event") defer span.End() @@ -73,6 +73,7 @@ func createPredeployEvent(ctx context.Context, client api.Client, applicationNam DeploymentTargetID: deploymentTargetID, } req.Metadata["start_time"] = createdAt + req.Metadata["app_revision_id"] = appRevisionID event, err := client.CreateOrUpdatePorterAppEvent(ctx, projectId, clusterId, applicationName, req) if err != nil { diff --git a/cli/cmd/v2/apply.go b/cli/cmd/v2/apply.go index 3eae1ffcc06..2190a38100f 100644 --- a/cli/cmd/v2/apply.go +++ b/cli/cmd/v2/apply.go @@ -141,14 +141,17 @@ func Apply(ctx context.Context, cliConf config.CLIConfig, client api.Client, por buildSettings.ProjectID = cliConf.Project err = build(ctx, client, buildSettings) + buildMetadata := make(map[string]interface{}) + buildMetadata["end_time"] = time.Now().UTC() + if err != nil { - _ = updateExistingEvent(ctx, client, appName, cliConf.Project, cliConf.Cluster, targetResp.DeploymentTargetID, eventID, types.PorterAppEventStatus_Failed, nil) + _ = updateExistingEvent(ctx, client, appName, cliConf.Project, cliConf.Cluster, targetResp.DeploymentTargetID, eventID, types.PorterAppEventStatus_Failed, buildMetadata) return fmt.Errorf("error building app: %w", err) } color.New(color.FgGreen).Printf("Successfully built image (tag: %s)\n", buildSettings.ImageTag) // nolint:errcheck,gosec - _ = updateExistingEvent(ctx, client, appName, cliConf.Project, cliConf.Cluster, targetResp.DeploymentTargetID, eventID, types.PorterAppEventStatus_Success, nil) + _ = updateExistingEvent(ctx, client, appName, cliConf.Project, cliConf.Cluster, targetResp.DeploymentTargetID, eventID, types.PorterAppEventStatus_Success, buildMetadata) applyResp, err = client.ApplyPorterApp(ctx, cliConf.Project, cliConf.Cluster, "", "", applyResp.AppRevisionId) if err != nil { @@ -160,7 +163,7 @@ func Apply(ctx context.Context, cliConf config.CLIConfig, client api.Client, por color.New(color.FgGreen).Printf("Waiting for predeploy to complete...\n") // nolint:errcheck,gosec now := time.Now().UTC() - eventID, _ := createPredeployEvent(ctx, client, appName, cliConf.Project, cliConf.Cluster, targetResp.DeploymentTargetID, now) + eventID, _ := createPredeployEvent(ctx, client, appName, cliConf.Project, cliConf.Cluster, targetResp.DeploymentTargetID, now, applyResp.AppRevisionId) eventStatus := types.PorterAppEventStatus_Success for { From d0bae300141377e22943cd18a954ed17d225ae69 Mon Sep 17 00:00:00 2001 From: ianedwards Date: Tue, 12 Sep 2023 15:36:46 -0400 Subject: [PATCH 16/59] POR-1683 move creation of porter domain to apply endpoint (#3551) --- api/server/handlers/porter_app/apply.go | 61 ++++++++++++++++++++ cli/cmd/v2/apply.go | 59 +------------------ internal/porter_app/create_subdomain.go | 77 +++++++++++++++++++++++++ 3 files changed, 139 insertions(+), 58 deletions(-) create mode 100644 internal/porter_app/create_subdomain.go diff --git a/api/server/handlers/porter_app/apply.go b/api/server/handlers/porter_app/apply.go index 41f65df8d4f..ddb5cb16081 100644 --- a/api/server/handlers/porter_app/apply.go +++ b/api/server/handlers/porter_app/apply.go @@ -1,7 +1,10 @@ package porter_app import ( + "context" "encoding/base64" + "errors" + "fmt" "net/http" "connectrpc.com/connect" @@ -10,8 +13,10 @@ import ( "github.com/porter-dev/api-contracts/generated/go/helpers" + "github.com/porter-dev/porter/internal/porter_app" "github.com/porter-dev/porter/internal/telemetry" + "github.com/porter-dev/porter/api/server/authz" "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" @@ -23,6 +28,7 @@ import ( // ApplyPorterAppHandler is the handler for the /apps/parse endpoint type ApplyPorterAppHandler struct { handlers.PorterHandlerReadWriter + authz.KubernetesAgentGetter } // NewApplyPorterAppHandler handles POST requests to the endpoint /apps/apply @@ -33,6 +39,7 @@ func NewApplyPorterAppHandler( ) *ApplyPorterAppHandler { return &ApplyPorterAppHandler{ PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer), + KubernetesAgentGetter: authz.NewOutOfClusterAgentGetter(config), } } @@ -115,6 +122,28 @@ func (c *ApplyPorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request telemetry.AttributeKV{Key: "app-name", Value: appProto.Name}, telemetry.AttributeKV{Key: "deployment-target-id", Value: request.DeploymentTargetId}, ) + + agent, err := c.GetAgent(r, cluster, "") + if err != nil { + err := telemetry.Error(ctx, span, err, "error getting kubernetes agent") + c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError)) + return + } + + subdomainCreateInput := porter_app.CreatePorterSubdomainInput{ + AppName: appProto.Name, + RootDomain: c.Config().ServerConf.AppRootDomain, + PowerDNSClient: c.Config().PowerDNSClient, + DNSRecordRepository: c.Repo().DNSRecord(), + KubernetesAgent: agent, + } + + appProto, err = addPorterSubdomainsIfNecessary(ctx, appProto, subdomainCreateInput) + if err != nil { + err := telemetry.Error(ctx, span, err, "error adding porter subdomains") + c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest)) + return + } } applyReq := connect.NewRequest(&porterv1.ApplyPorterAppRequest{ @@ -164,3 +193,35 @@ func (c *ApplyPorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request c.WriteResult(w, r, response) } + +// addPorterSubdomainsIfNecessary adds porter subdomains to the app proto if a web service is changed to private and has no domains +func addPorterSubdomainsIfNecessary(ctx context.Context, app *porterv1.PorterApp, createSubdomainInput porter_app.CreatePorterSubdomainInput) (*porterv1.PorterApp, error) { + for serviceName, service := range app.Services { + if service.Type == porterv1.ServiceType_SERVICE_TYPE_WEB { + if service.GetWebConfig() == nil { + return app, fmt.Errorf("web service %s does not contain web config", serviceName) + } + + webConfig := service.GetWebConfig() + + if !webConfig.Private && len(webConfig.Domains) == 0 { + subdomain, err := porter_app.CreatePorterSubdomain(ctx, createSubdomainInput) + if err != nil { + return app, fmt.Errorf("error creating subdomain: %w", err) + } + + if subdomain == "" { + return app, errors.New("response subdomain is empty") + } + + webConfig.Domains = []*porterv1.Domain{ + {Name: subdomain}, + } + + service.Config = &porterv1.Service_WebConfig{WebConfig: webConfig} + } + } + } + + return app, nil +} diff --git a/cli/cmd/v2/apply.go b/cli/cmd/v2/apply.go index 2190a38100f..41b860289e7 100644 --- a/cli/cmd/v2/apply.go +++ b/cli/cmd/v2/apply.go @@ -90,12 +90,7 @@ func Apply(ctx context.Context, cliConf config.CLIConfig, client api.Client, por return fmt.Errorf("error creating porter app db entry: %w", err) } - base64AppProtoWithSubdomains, err := addPorterSubdomainsIfNecessary(ctx, client, cliConf.Project, cliConf.Cluster, base64AppProto) - if err != nil { - return fmt.Errorf("error creating subdomains: %w", err) - } - - applyResp, err := client.ApplyPorterApp(ctx, cliConf.Project, cliConf.Cluster, base64AppProtoWithSubdomains, targetResp.DeploymentTargetID, "") + applyResp, err := client.ApplyPorterApp(ctx, cliConf.Project, cliConf.Cluster, base64AppProto, targetResp.DeploymentTargetID, "") if err != nil { return fmt.Errorf("error calling apply endpoint: %w", err) } @@ -273,58 +268,6 @@ func createPorterAppDbEntryInputFromProtoAndEnv(base64AppProto string) (api.Crea return input, fmt.Errorf("app does not contain build or image settings") } -func addPorterSubdomainsIfNecessary(ctx context.Context, client api.Client, project uint, cluster uint, base64AppProto string) (string, error) { - var editedB64AppProto string - - decoded, err := base64.StdEncoding.DecodeString(base64AppProto) - if err != nil { - return editedB64AppProto, fmt.Errorf("unable to decode base64 app for revision: %w", err) - } - - app := &porterv1.PorterApp{} - err = helpers.UnmarshalContractObject(decoded, app) - if err != nil { - return editedB64AppProto, fmt.Errorf("unable to unmarshal app for revision: %w", err) - } - - for serviceName, service := range app.Services { - if service.Type == porterv1.ServiceType_SERVICE_TYPE_WEB { - if service.GetWebConfig() == nil { - return editedB64AppProto, fmt.Errorf("web service %s does not contain web config", serviceName) - } - - webConfig := service.GetWebConfig() - - if !webConfig.Private && len(webConfig.Domains) == 0 { - color.New(color.FgYellow).Printf("Service %s is public but does not contain any domains, creating Porter domain\n", serviceName) // nolint:errcheck,gosec - domain, err := client.CreateSubdomain(ctx, project, cluster, app.Name, serviceName) - if err != nil { - return editedB64AppProto, fmt.Errorf("error creating subdomain: %w", err) - } - - if domain.Subdomain == "" { - return editedB64AppProto, errors.New("response subdomain is empty") - } - - webConfig.Domains = []*porterv1.Domain{ - {Name: domain.Subdomain}, - } - - service.Config = &porterv1.Service_WebConfig{WebConfig: webConfig} - } - } - } - - marshalled, err := helpers.MarshalContractObject(ctx, app) - if err != nil { - return editedB64AppProto, fmt.Errorf("unable to marshal app back to json: %w", err) - } - - editedB64AppProto = base64.StdEncoding.EncodeToString(marshalled) - - return editedB64AppProto, nil -} - func buildSettingsFromBase64AppProto(base64AppProto string) (buildInput, error) { var buildSettings buildInput diff --git a/internal/porter_app/create_subdomain.go b/internal/porter_app/create_subdomain.go new file mode 100644 index 00000000000..c30faec1b40 --- /dev/null +++ b/internal/porter_app/create_subdomain.go @@ -0,0 +1,77 @@ +package porter_app + +import ( + "context" + + "github.com/porter-dev/porter/internal/integrations/powerdns" + "github.com/porter-dev/porter/internal/kubernetes" + "github.com/porter-dev/porter/internal/kubernetes/domain" + "github.com/porter-dev/porter/internal/repository" + "github.com/porter-dev/porter/internal/telemetry" +) + +// CreatePorterSubdomainInput is the input to the CreatePorterSubdomain function +type CreatePorterSubdomainInput struct { + AppName string + RootDomain string + KubernetesAgent *kubernetes.Agent + PowerDNSClient *powerdns.Client + DNSRecordRepository repository.DNSRecordRepository +} + +// CreatePorterSubdomain creates a subdomain for the porter app +func CreatePorterSubdomain(ctx context.Context, input CreatePorterSubdomainInput) (string, error) { + ctx, span := telemetry.NewSpan(ctx, "create-porter-subdomain") + defer span.End() + + var createdDomain string + + if input.KubernetesAgent == nil { + return "", telemetry.Error(ctx, span, nil, "k8s agent is nil") + } + if input.PowerDNSClient == nil { + return "", telemetry.Error(ctx, span, nil, "powerdns client is nil") + } + if input.AppName == "" { + return "", telemetry.Error(ctx, span, nil, "app name is empty") + } + if input.RootDomain == "" { + return "", telemetry.Error(ctx, span, nil, "root domain is empty") + } + + endpoint, found, err := domain.GetNGINXIngressServiceIP(input.KubernetesAgent.Clientset) + if err != nil { + return createdDomain, telemetry.Error(ctx, span, err, "error getting nginx ingress service ip") + } + if !found { + return createdDomain, telemetry.Error(ctx, span, nil, "target cluster does not have nginx ingress") + } + + createDomainConf := domain.CreateDNSRecordConfig{ + ReleaseName: input.AppName, + RootDomain: input.RootDomain, + Endpoint: endpoint, + } + + record := createDomainConf.NewDNSRecordForEndpoint() + + record, err = input.DNSRecordRepository.CreateDNSRecord(record) + + if err != nil { + return createdDomain, telemetry.Error(ctx, span, nil, "error creating dns record") + } + if record == nil { + return createdDomain, telemetry.Error(ctx, span, nil, "dns record is nil") + } + + _record := domain.DNSRecord(*record) + + err = _record.CreateDomain(input.PowerDNSClient) + if err != nil { + return createdDomain, telemetry.Error(ctx, span, err, "error creating domain") + } + + createdDomain = _record.Hostname + + return createdDomain, nil +} From a6c7293ffeab08d6b5769bad007e450a2c1103d2 Mon Sep 17 00:00:00 2001 From: Feroze Mohideen Date: Tue, 12 Sep 2023 15:52:59 -0400 Subject: [PATCH 17/59] Port over more activity feed subviews to v2 porter yaml (#3553) --- dashboard/src/components/porter/Button.tsx | 4 +- dashboard/src/lib/porter-apps/index.ts | 16 +- dashboard/src/lib/porter-apps/services.ts | 44 ++--- .../app-view/AppDataContainer.tsx | 19 ++- .../app-dashboard/app-view/tabs/LogsTab.tsx | 33 ++-- .../events/cards/DeployEventCard.tsx | 14 +- .../activity-feed/events/cards/EventCard.tsx | 2 +- .../BuildFailureEventFocusView.tsx | 31 ++-- .../focus-views/DeployEventFocusView.tsx | 71 -------- .../events/focus-views/EventFocusView.tsx | 120 ++++++------- .../focus-views/PredeployEventFocusView.tsx | 29 ++-- .../tabs/activity-feed/events/types.ts | 1 + .../tabs/activity-feed/events/utils.ts | 14 +- .../validate-apply/logs/Logs.tsx | 27 +-- .../validate-apply/metrics/MetricsSection.tsx | 158 +++++++++--------- .../services-settings/tabs/CustomDomains.tsx | 1 - 16 files changed, 264 insertions(+), 320 deletions(-) delete mode 100644 dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/focus-views/DeployEventFocusView.tsx diff --git a/dashboard/src/components/porter/Button.tsx b/dashboard/src/components/porter/Button.tsx index 3aa3cdc9853..b997f79ede7 100644 --- a/dashboard/src/components/porter/Button.tsx +++ b/dashboard/src/components/porter/Button.tsx @@ -38,7 +38,7 @@ const Button: React.FC = ({ withBorder, rounded, alt, - type, + type = "button", disabledTooltipMessage, }) => { const renderStatus = () => { @@ -194,7 +194,7 @@ const StyledButton = styled.button<{ : props.color || props.theme.button; }}; display: flex; - ailgn-items: center; + align-items: center; justify-content: center; border-radius: ${(props) => (props.rounded ? "50px" : "5px")}; border: ${(props) => (props.withBorder ? "1px solid #494b4f" : "none")}; diff --git a/dashboard/src/lib/porter-apps/index.ts b/dashboard/src/lib/porter-apps/index.ts index 09419facef4..8f78817df29 100644 --- a/dashboard/src/lib/porter-apps/index.ts +++ b/dashboard/src/lib/porter-apps/index.ts @@ -308,15 +308,15 @@ export function clientAppFromProto( const predeployOverrides = serializeService(overrides.predeploy); const predeploy = proto.predeploy ? [ - deserializeService({ - service: serializedServiceFromProto({ - name: "pre-deploy", - service: proto.predeploy, - isPredeploy: true, - }), - override: predeployOverrides, + deserializeService({ + service: serializedServiceFromProto({ + name: "pre-deploy", + service: proto.predeploy, + isPredeploy: true, }), - ] + override: predeployOverrides, + }), + ] : undefined; return { diff --git a/dashboard/src/lib/porter-apps/services.ts b/dashboard/src/lib/porter-apps/services.ts index 2009f0358f6..dd232deff27 100644 --- a/dashboard/src/lib/porter-apps/services.ts +++ b/dashboard/src/lib/porter-apps/services.ts @@ -72,27 +72,27 @@ export type SerializedService = { cpuCores: number; ramMegabytes: number; config: - | { - type: "web"; - domains: { - name: string; - }[]; - autoscaling?: SerializedAutoscaling; - healthCheck?: SerializedHealthcheck; - private: boolean; - } - | { - type: "worker"; - autoscaling?: SerializedAutoscaling; - } - | { - type: "job"; - allowConcurrent: boolean; - cron: string; - } - | { - type: "predeploy"; - }; + | { + type: "web"; + domains: { + name: string; + }[]; + autoscaling?: SerializedAutoscaling; + healthCheck?: SerializedHealthcheck; + private: boolean; + } + | { + type: "worker"; + autoscaling?: SerializedAutoscaling; + } + | { + type: "job"; + allowConcurrent: boolean; + cron: string; + } + | { + type: "predeploy"; + }; }; export function isPredeployService(service: SerializedService | ClientService) { @@ -285,7 +285,7 @@ export function deserializeService({ health: config.healthCheck, override: overrideWebConfig?.healthCheck, }), - domains: config.domains.map((domain) => ({ + domains: [...config.domains, ...(overrideWebConfig?.domains ?? [])].map((domain) => ({ name: ServiceField.string( domain.name, overrideWebConfig?.domains.find( diff --git a/dashboard/src/main/home/app-dashboard/app-view/AppDataContainer.tsx b/dashboard/src/main/home/app-dashboard/app-view/AppDataContainer.tsx index 50a9d506fc9..3b05fe44aa0 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/AppDataContainer.tsx +++ b/dashboard/src/main/home/app-dashboard/app-view/AppDataContainer.tsx @@ -28,12 +28,13 @@ import LogsTab from "./tabs/LogsTab"; import MetricsTab from "./tabs/MetricsTab"; import RevisionsList from "../validate-apply/revisions-list/RevisionsList"; import Activity from "./tabs/Activity"; +import EventFocusView from "./tabs/activity-feed/events/focus-views/EventFocusView"; // commented out tabs are not yet implemented // will be included as support is available based on data from app revisions rather than helm releases const validTabs = [ "activity", - // "events", + "events", "overview", "logs", "metrics", @@ -197,7 +198,10 @@ const AppDataContainer: React.FC = ({ tabParam }) => { porterApp.name, ]); setPreviewRevision(null); - } catch (err) {} + + // redirect to the default tab after save + history.push(`/apps/${porterApp.name}/${DEFAULT_TAB}`); + } catch (err) { } }); useEffect(() => { @@ -261,11 +265,11 @@ const AppDataContainer: React.FC = ({ tabParam }) => { { label: "Environment", value: "environment" }, ...(latestProto.build ? [ - { - label: "Build Settings", - value: "build-settings", - }, - ] + { + label: "Build Settings", + value: "build-settings", + }, + ] : []), { label: "Settings", value: "settings" }, ]} @@ -288,6 +292,7 @@ const AppDataContainer: React.FC = ({ tabParam }) => { .with("settings", () => ) .with("logs", () => ) .with("metrics", () => ) + .with("events", () => ) .otherwise(() => null)} diff --git a/dashboard/src/main/home/app-dashboard/app-view/tabs/LogsTab.tsx b/dashboard/src/main/home/app-dashboard/app-view/tabs/LogsTab.tsx index 1d1a2bbd7d1..b662eeb9e02 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/tabs/LogsTab.tsx +++ b/dashboard/src/main/home/app-dashboard/app-view/tabs/LogsTab.tsx @@ -1,35 +1,22 @@ -import { PorterApp } from "@porter-dev/api-contracts"; -import Spacer from "components/porter/Spacer"; -import Text from "components/porter/Text"; -import { PorterAppFormData } from "lib/porter-apps"; -import React, { useMemo } from "react"; -import { useFormContext, useFormState } from "react-hook-form"; +import React from "react"; import Logs from "../../validate-apply/logs/Logs" -import { - defaultSerialized, - deserializeService, -} from "lib/porter-apps/services"; -import Error from "components/porter/Error"; -import Button from "components/porter/Button"; import { useLatestRevision } from "../LatestRevisionContext"; const LogsTab: React.FC = () => { - const { projectId, clusterId, latestProto , deploymentTargetId, latestRevision} = useLatestRevision(); + const { projectId, clusterId, latestProto, deploymentTargetId, latestRevision } = useLatestRevision(); const appName = latestProto.name const serviceNames = Object.keys(latestProto.services) return ( - <> - - + ); }; diff --git a/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/DeployEventCard.tsx b/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/DeployEventCard.tsx index 7858f6576e8..6aae30f543b 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/DeployEventCard.tsx +++ b/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/DeployEventCard.tsx @@ -12,19 +12,23 @@ import { PorterAppDeployEvent } from "../types"; import AnimateHeight from "react-animate-height"; import ServiceStatusDetail from "./ServiceStatusDetail"; import { useLatestRevision } from "main/home/app-dashboard/app-view/LatestRevisionContext"; +import { useRevisionIdToNumber } from "lib/hooks/useRevisionList"; type Props = { event: PorterAppDeployEvent; appName: string; showServiceStatusDetail?: boolean; + deploymentTargetId: string; }; -const DeployEventCard: React.FC = ({ event, appName, showServiceStatusDetail = false }) => { +const DeployEventCard: React.FC = ({ event, appName, deploymentTargetId, showServiceStatusDetail = false }) => { const { latestRevision } = useLatestRevision(); const [diffModalVisible, setDiffModalVisible] = useState(false); const [revertModalVisible, setRevertModalVisible] = useState(false); const [serviceStatusVisible, setServiceStatusVisible] = useState(showServiceStatusDetail); + const revisionIdToNumber = useRevisionIdToNumber(appName, deploymentTargetId); + const renderStatusText = () => { switch (event.status) { case "SUCCESS": @@ -131,7 +135,7 @@ const DeployEventCard: React.FC = ({ event, appName, showServiceStatusDet - Application version no. {event.metadata?.revision} + Application version no. {revisionIdToNumber[event.metadata.app_revision_id]} @@ -140,12 +144,12 @@ const DeployEventCard: React.FC = ({ event, appName, showServiceStatusDet {renderStatusText()} - {latestRevision.id !== event.metadata.app_revision_id && ( + {revisionIdToNumber[event.metadata.app_revision_id] != null && latestRevision.revision_number !== revisionIdToNumber[event.metadata.app_revision_id] && ( <> setRevertModalVisible(true)}> - Revert to version {event.metadata.revision} + Revert to version {revisionIdToNumber[event.metadata.app_revision_id]} @@ -184,7 +188,7 @@ const DeployEventCard: React.FC = ({ event, appName, showServiceStatusDet } diff --git a/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/EventCard.tsx b/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/EventCard.tsx index 7b61d374fed..e98f33530cd 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/EventCard.tsx +++ b/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/EventCard.tsx @@ -21,7 +21,7 @@ const EventCard: React.FC = ({ event, deploymentTargetId, isLatestDeployE return match(event) .with({ type: "APP_EVENT" }, (ev) => ) .with({ type: "BUILD" }, (ev) => ) - .with({ type: "DEPLOY" }, (ev) => ) + .with({ type: "DEPLOY" }, (ev) => ) .with({ type: "PRE_DEPLOY" }, (ev) => ) .exhaustive(); }; diff --git a/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/focus-views/BuildFailureEventFocusView.tsx b/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/focus-views/BuildFailureEventFocusView.tsx index 44a1383b554..8309be4f22d 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/focus-views/BuildFailureEventFocusView.tsx +++ b/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/focus-views/BuildFailureEventFocusView.tsx @@ -10,18 +10,19 @@ import Text from "components/porter/Text"; import { readableDate } from "shared/string_utils"; import { getDuration } from "../utils"; import Link from "components/porter/Link"; -import { PorterLog } from "../../../logs/types"; -import { PorterAppEvent } from "../types"; +import { PorterAppBuildEvent } from "../types"; +import { PorterLog } from "main/home/app-dashboard/expanded-app/logs/types"; +import { useLatestRevision } from "main/home/app-dashboard/app-view/LatestRevisionContext"; type Props = { - event: PorterAppEvent; - appData: any; + event: PorterAppBuildEvent; }; const BuildFailureEventFocusView: React.FC = ({ event, - appData, }) => { + const { porterApp, projectId, clusterId } = useLatestRevision(); + const [logs, setLogs] = useState([]); const [isLoading, setIsLoading] = useState(true); const scrollToBottomRef = useRef(null); @@ -36,7 +37,7 @@ const BuildFailureEventFocusView: React.FC = ({ }, [isLoading, logs, scrollToBottomRef]); const getBuildLogs = async () => { - if (event == null) { + if (event == null || porterApp.git_repo_id == null || porterApp.repo_name == null) { return; } try { @@ -46,13 +47,13 @@ const BuildFailureEventFocusView: React.FC = ({ "", {}, { - project_id: appData.app.project_id, - cluster_id: appData.app.cluster_id, - git_installation_id: appData.app.git_repo_id, - owner: appData.app.repo_name?.split("/")[0], - name: appData.app.repo_name?.split("/")[1], - filename: "porter_stack_" + appData.chart.name + ".yml", - run_id: event.metadata.action_run_id, + project_id: projectId, + cluster_id: clusterId, + git_installation_id: porterApp.git_repo_id, + owner: porterApp.repo_name.split("/")[0], + name: porterApp.repo_name.split("/")[1], + filename: "porter_stack_" + porterApp.name + ".yml", + run_id: event.metadata.action_run_id.toString(), } ); let logs: PorterLog[] = []; @@ -175,8 +176,8 @@ const BuildFailureEventFocusView: React.FC = ({ target="_blank" to={ event.metadata.action_run_id - ? `https://github.com/${appData.app.repo_name}/actions/runs/${event.metadata.action_run_id}` - : `https://github.com/${appData.app.repo_name}/actions` + ? `https://github.com/${porterApp.repo_name}/actions/runs/${event.metadata.action_run_id}` + : `https://github.com/${porterApp.repo_name}/actions` } > View full build logs diff --git a/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/focus-views/DeployEventFocusView.tsx b/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/focus-views/DeployEventFocusView.tsx deleted file mode 100644 index 69c8cc925bf..00000000000 --- a/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/focus-views/DeployEventFocusView.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import Spacer from "components/porter/Spacer"; -import React from "react"; -import dayjs from "dayjs"; -import Text from "components/porter/Text"; -import { readableDate } from "shared/string_utils"; -import { getDuration } from "../utils"; -import LogSection from "../../../logs/LogSection"; -import { AppearingView } from "./EventFocusView"; -import Icon from "components/porter/Icon"; -import loading from "assets/loading.gif"; -import Container from "components/porter/Container"; -import { PorterAppDeployEvent } from "../types"; -import { LogFilterQueryParamOpts } from "../../../logs/types"; - -type Props = { - event: PorterAppDeployEvent; - appData: any; - filterOpts?: LogFilterQueryParamOpts -}; - -const DeployEventFocusView: React.FC = ({ - event, - appData, - filterOpts, -}) => { - const renderHeaderText = () => { - switch (event.status) { - case "SUCCESS": - return Deploy succeeded; - case "FAILED": - return Deploy failed; - case "CANCELED": - return Deploy canceled; - default: - return ( - - - - Deploy in progress... - - ); - } - }; - - const renderDurationText = () => { - switch (event.status) { - case "PROGRESSING": - return Started {readableDate(event.created_at)}. - default: - return Started {readableDate(event.created_at)} and ran for {getDuration(event)}.; - } - } - - return ( - <> - - {renderHeaderText()} - - - {renderDurationText()} - - - - ); -}; - -export default DeployEventFocusView; \ No newline at end of file diff --git a/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/focus-views/EventFocusView.tsx b/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/focus-views/EventFocusView.tsx index 0206740f1bb..5c3df07663d 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/focus-views/EventFocusView.tsx +++ b/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/focus-views/EventFocusView.tsx @@ -1,90 +1,90 @@ import Loading from "components/Loading"; import Spacer from "components/porter/Spacer"; -import React, { useContext, useEffect, useState } from "react"; -import { Context } from "shared/Context"; +import React, { useEffect, useState } from "react"; import api from "shared/api"; import styled from "styled-components"; import Link from "components/porter/Link"; import BuildFailureEventFocusView from "./BuildFailureEventFocusView"; import PreDeployEventFocusView from "./PredeployEventFocusView"; import _ from "lodash"; -import { PorterAppEvent, porterAppEventValidator } from "../types"; -import DeployEventFocusView from "./DeployEventFocusView"; -import { LogFilterQueryParamOpts } from "../../../logs/types"; - -type Props = { - eventId: string; - appData: any; - filterOpts?: LogFilterQueryParamOpts; -}; +import { PorterAppBuildEvent, PorterAppPreDeployEvent, porterAppEventValidator } from "../types"; +import { useLocation } from "react-router"; +import { useLatestRevision } from "main/home/app-dashboard/app-view/LatestRevisionContext"; +import { useQuery } from "@tanstack/react-query"; +import { match } from "ts-pattern"; const EVENT_POLL_INTERVAL = 5000; // poll every 5 seconds -const EventFocusView: React.FC = ({ - eventId, - appData, - filterOpts, -}) => { - const { currentProject, currentCluster } = useContext(Context); - const [event, setEvent] = useState(null); +type SupportedEventFocusViewEvent = PorterAppBuildEvent | PorterAppPreDeployEvent; - useEffect(() => { - const getEvent = async () => { - if (currentProject == null || currentCluster == null) { - return; +const EventFocusView: React.FC = ({ }) => { + const { search } = useLocation(); + const queryParams = new URLSearchParams(search); + const eventId = queryParams.get("event_id"); + const { projectId, clusterId, latestProto, deploymentTargetId, latestRevision } = useLatestRevision(); + + const [event, setEvent] = useState(null); + + const { data } = useQuery( + [ + "getPorterAppEvent", + projectId, + clusterId, + eventId, + event, + ], + async () => { + if (eventId == null || eventId === "") { + return null; } - try { - const eventResp = await api.getPorterAppEvent( - "", - {}, - { - project_id: currentProject.id, - cluster_id: currentCluster.id, - event_id: eventId, - } - ) - const newEvent = porterAppEventValidator.parse(eventResp.data.event); - setEvent(newEvent); - if (newEvent.metadata?.end_time != null) { - clearInterval(intervalId); + const eventResp = await api.getPorterAppEvent( + "", + {}, + { + project_id: projectId, + cluster_id: clusterId, + event_id: eventId, } - } catch (err) { - console.log(err); - } + ); + return porterAppEventValidator.parse(eventResp.data.event); + }, + { + // last condition checks if the event is done running; then we stop refetching + enabled: eventId != null && eventId !== "" && !(event != null && event.metadata.end_time != null), + refetchInterval: EVENT_POLL_INTERVAL, } - const intervalId = setInterval(getEvent, EVENT_POLL_INTERVAL); - getEvent(); - return () => clearInterval(intervalId); - }, []); + ); - const getEventFocusView = (event: PorterAppEvent, appData: any) => { - switch (event.type) { - case "BUILD": - return - case "PRE_DEPLOY": - return - case "DEPLOY": - return - default: - return null + useEffect(() => { + if (data != null && (data.type === "BUILD" || data.type === "PRE_DEPLOY")) { + setEvent(data); } + }, [data]); + + const getEventFocusView = () => { + return match(event) + .with({ type: "BUILD" }, (ev) => ) + .with({ type: "PRE_DEPLOY" }, (ev) => ) + .with(null, () => { + if (eventId != null && eventId !== "") { + return ; + } else { + return
Event not found
; + } + }) + .exhaustive(); } return ( - + keyboard_backspace Activity feed - {event == null && } - {event != null && getEventFocusView(event, appData)} + {getEventFocusView()} ); }; diff --git a/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/focus-views/PredeployEventFocusView.tsx b/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/focus-views/PredeployEventFocusView.tsx index 2bdbfccebf7..dc2c3cfd877 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/focus-views/PredeployEventFocusView.tsx +++ b/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/focus-views/PredeployEventFocusView.tsx @@ -4,22 +4,26 @@ import dayjs from "dayjs"; import Text from "components/porter/Text"; import { readableDate } from "shared/string_utils"; import { getDuration } from "../utils"; -import LogSection from "../../../logs/LogSection"; import { AppearingView } from "./EventFocusView"; import Icon from "components/porter/Icon"; import loading from "assets/loading.gif"; import Container from "components/porter/Container"; -import { PorterAppEvent } from "../types"; +import { PorterAppPreDeployEvent } from "../types"; +import Logs from "main/home/app-dashboard/validate-apply/logs/Logs"; +import { useLatestRevision } from "main/home/app-dashboard/app-view/LatestRevisionContext"; type Props = { - event: PorterAppEvent; - appData: any; + event: PorterAppPreDeployEvent; }; const PreDeployEventFocusView: React.FC = ({ event, - appData, }) => { + const { projectId, clusterId, latestProto, deploymentTargetId, latestRevision } = useLatestRevision(); + + const appName = latestProto.name + const serviceNames = [`${latestProto.name}-predeploy`] + const renderHeaderText = () => { switch (event.status) { case "SUCCESS": @@ -54,14 +58,13 @@ const PreDeployEventFocusView: React.FC = ({ {renderDurationText()} - ); diff --git a/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/types.ts b/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/types.ts index 744f701b79c..431b79f3dd8 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/types.ts +++ b/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/types.ts @@ -25,6 +25,7 @@ const porterAppBuildEventMetadataValidator = z.object({ repo: z.string(), action_run_id: z.number(), github_account_id: z.number(), + end_time: z.string().optional(), }) const porterAppPreDeployEventMetadataValidator = z.object({ start_time: z.string(), diff --git a/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/utils.ts b/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/utils.ts index a2e8ad672a8..8e3cf1763fd 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/utils.ts +++ b/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/utils.ts @@ -3,13 +3,17 @@ import failure from "assets/failure.svg"; import loading from "assets/loading.gif"; import canceled from "assets/canceled.svg" import api from "shared/api"; -import { PorterAppEvent } from "./types"; -import { SourceOptions } from "lib/porter-apps"; +import { PorterAppBuildEvent, PorterAppEvent, PorterAppPreDeployEvent } from "./types"; import { PorterAppRecord } from "../../../AppView"; +import { match } from "ts-pattern"; -export const getDuration = (event: PorterAppEvent): string => { - const startTimeStamp = new Date(event.metadata.start_time ?? event.created_at).getTime(); - const endTimeStamp = new Date(event.metadata.end_time ?? event.updated_at).getTime(); +export const getDuration = (event: PorterAppPreDeployEvent | PorterAppBuildEvent): string => { + const startTimeStamp = match(event) + .with({ type: "BUILD" }, (ev) => new Date(ev.created_at).getTime()) + .with({ type: "PRE_DEPLOY" }, (ev) => new Date(ev.metadata.start_time).getTime()) + .exhaustive(); + + const endTimeStamp = event.metadata.end_time ? new Date(event.metadata.end_time).getTime() : Date.now() const timeDifferenceMilliseconds = endTimeStamp - startTimeStamp; diff --git a/dashboard/src/main/home/app-dashboard/validate-apply/logs/Logs.tsx b/dashboard/src/main/home/app-dashboard/validate-apply/logs/Logs.tsx index a29a3deec76..4d8381466ef 100644 --- a/dashboard/src/main/home/app-dashboard/validate-apply/logs/Logs.tsx +++ b/dashboard/src/main/home/app-dashboard/validate-apply/logs/Logs.tsx @@ -23,12 +23,11 @@ import Text from "components/porter/Text"; import Spacer from "components/porter/Spacer"; import Container from "components/porter/Container"; import Button from "components/porter/Button"; -import { Service } from "../../new-app-flow/serviceTypes"; import LogFilterContainer from "../../expanded-app/logs/LogFilterContainer"; import StyledLogs from "../../expanded-app/logs/StyledLogs"; -import {z} from "zod"; -import {AppRevision, appRevisionValidator} from "lib/revisions/types"; -import {useLatestRevisionNumber, useRevisionIdToNumber} from "lib/hooks/useRevisionList"; +import { AppRevision } from "lib/revisions/types"; +import { useLatestRevisionNumber, useRevisionIdToNumber } from "lib/hooks/useRevisionList"; +import { useLocation } from "react-router"; type Props = { projectId: number; @@ -47,6 +46,14 @@ const Logs: React.FC = ({ deploymentTargetId, latestRevision, }) => { + const { search } = useLocation(); + const queryParams = new URLSearchParams(search); + const logQueryParamOpts = { + revision: queryParams.get('version'), + output_stream: queryParams.get('output_stream'), + service: queryParams.get('service'), + } + const scrollToBottomRef = useRef(undefined); const [scrollToBottomEnabled, setScrollToBottomEnabled] = useState(true); const [enteredSearchText, setEnteredSearchText] = useState(""); @@ -60,10 +67,10 @@ const Logs: React.FC = ({ const [logsError, setLogsError] = useState(undefined); const [selectedFilterValues, setSelectedFilterValues] = useState>({ - service_name: GenericLogFilter.getDefaultOption("service_name").value, + service_name: logQueryParamOpts?.service ?? GenericLogFilter.getDefaultOption("service_name").value, pod_name: "", // not supported - revision: GenericLogFilter.getDefaultOption("revision").value, - output_stream: GenericLogFilter.getDefaultOption("output_stream").value, + revision: logQueryParamOpts.revision ?? GenericLogFilter.getDefaultOption("revision").value, + output_stream: logQueryParamOpts.output_stream ?? GenericLogFilter.getDefaultOption("output_stream").value, }); const revisionIdToNumber = useRevisionIdToNumber(appName, deploymentTargetId) @@ -235,10 +242,10 @@ const Logs: React.FC = ({ const resetFilters = () => { setSelectedFilterValues({ - output_stream: GenericLogFilter.getDefaultOption("output_stream").value, + service_name: logQueryParamOpts?.service ?? GenericLogFilter.getDefaultOption("service_name").value, pod_name: "", // not supported - revision: GenericLogFilter.getDefaultOption("revision").value, - service_name: GenericLogFilter.getDefaultOption("service_name").value, + revision: logQueryParamOpts.revision ?? GenericLogFilter.getDefaultOption("revision").value, + output_stream: logQueryParamOpts.output_stream ?? GenericLogFilter.getDefaultOption("output_stream").value, }); }; diff --git a/dashboard/src/main/home/app-dashboard/validate-apply/metrics/MetricsSection.tsx b/dashboard/src/main/home/app-dashboard/validate-apply/metrics/MetricsSection.tsx index d7c2b94d619..f0433479267 100644 --- a/dashboard/src/main/home/app-dashboard/validate-apply/metrics/MetricsSection.tsx +++ b/dashboard/src/main/home/app-dashboard/validate-apply/metrics/MetricsSection.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useMemo, useState} from "react"; +import React, { useEffect, useMemo, useState } from "react"; import styled from "styled-components"; import api from "shared/api"; @@ -13,97 +13,101 @@ import MetricsChart from "../../expanded-app/metrics/MetricsChart"; import { useQuery } from "@tanstack/react-query"; import Loading from "components/Loading"; import CheckboxRow from "components/CheckboxRow"; -import {PorterApp} from "@porter-dev/api-contracts"; +import { PorterApp } from "@porter-dev/api-contracts"; +import { useLocation } from "react-router"; type PropsType = { - projectId: number; - clusterId: number; - appName: string; - services: PorterApp["services"]; - deploymentTargetId: string; + projectId: number; + clusterId: number; + appName: string; + services: PorterApp["services"]; + deploymentTargetId: string; }; type ServiceOption = { - label: string; - value: string; + label: string; + value: string; } const MetricsSection: React.FunctionComponent = ({ - projectId, - clusterId, - appName, - services, - deploymentTargetId, + projectId, + clusterId, + appName, + services, + deploymentTargetId, }) => { - const [selectedServiceName, setSelectedServiceName] = useState(""); + const { search } = useLocation(); + const queryParams = new URLSearchParams(search); + const serviceFromQueryParams = queryParams.get("service"); + + const [selectedServiceName, setSelectedServiceName] = useState(serviceFromQueryParams ?? ""); const [selectedRange, setSelectedRange] = useState("1H"); const [showAutoscalingThresholds, setShowAutoscalingThresholds] = useState(true); - const serviceOptions: ServiceOption[] = useMemo(() => { - return Object.keys(services).map((name) => { - return { - label: name, - value: name, - }; - }); + return Object.keys(services).map((name) => { + return { + label: name, + value: name, + }; + }); }, [services]); - useEffect(() => { - if (serviceOptions.length > 0) { - setSelectedServiceName(serviceOptions[0].value) - } - }, []); + useEffect(() => { + if (serviceOptions.length > 0 && selectedServiceName === "") { + setSelectedServiceName(serviceOptions[0].value) + } + }, []); - const [serviceName, serviceKind, metricTypes, isHpaEnabled] = useMemo(() => { - if (selectedServiceName === "") { - return ["", "", [], false] - } + const [serviceName, serviceKind, metricTypes, isHpaEnabled] = useMemo(() => { + if (selectedServiceName === "") { + return ["", "", [], false] + } - const service = services[selectedServiceName] + const service = services[selectedServiceName] - const serviceName = service.absoluteName === "" ? (appName + "-" + selectedServiceName) : service.absoluteName + const serviceName = service.absoluteName === "" ? (appName + "-" + selectedServiceName) : service.absoluteName - let serviceKind = "" - const metricTypes: MetricType[] = ["cpu", "memory"]; - let isHpaEnabled = false + let serviceKind = "" + const metricTypes: MetricType[] = ["cpu", "memory"]; + let isHpaEnabled = false - if (service.config.case === "webConfig") { - serviceKind = "web" - metricTypes.push("network"); - if (service.config.value.autoscaling != null && service.config.value.autoscaling.enabled) { - isHpaEnabled = true - } - if (!service.config.value.private) { - metricTypes.push("nginx:status") - } - } + if (service.config.case === "webConfig") { + serviceKind = "web" + metricTypes.push("network"); + if (service.config.value.autoscaling != null && service.config.value.autoscaling.enabled) { + isHpaEnabled = true + } + if (!service.config.value.private) { + metricTypes.push("nginx:status") + } + } - if (service.config.case === "workerConfig") { - serviceKind = "worker" - if (service.config.value.autoscaling != null && service.config.value.autoscaling.enabled) { - isHpaEnabled = true - } - } + if (service.config.case === "workerConfig") { + serviceKind = "worker" + if (service.config.value.autoscaling != null && service.config.value.autoscaling.enabled) { + isHpaEnabled = true + } + } - if (isHpaEnabled) { - metricTypes.push("hpa_replicas"); - } + if (isHpaEnabled) { + metricTypes.push("hpa_replicas"); + } - return [serviceName, serviceKind, metricTypes, isHpaEnabled] - }, [selectedServiceName]) + return [serviceName, serviceKind, metricTypes, isHpaEnabled] + }, [selectedServiceName]) const { data: metricsData, isLoading: isMetricsDataLoading, refetch } = useQuery( [ - "getMetrics", - projectId, - clusterId, - serviceName, - selectedRange, - deploymentTargetId, + "getMetrics", + projectId, + clusterId, + serviceName, + selectedRange, + deploymentTargetId, ], async () => { @@ -118,17 +122,17 @@ const MetricsSection: React.FunctionComponent = ({ const start = end - secondsBeforeNow[selectedRange]; for (const metricType of metricTypes) { - var kind = ""; - if (serviceKind === "web") { - kind = "deployment"; - } else if (serviceKind === "worker") { - kind = "deployment"; - } else if (serviceKind === "job") { - kind = "job"; - } - if (metricType === "nginx:status") { - kind = "Ingress" - } + var kind = ""; + if (serviceKind === "web") { + kind = "deployment"; + } else if (serviceKind === "worker") { + kind = "deployment"; + } else if (serviceKind === "job") { + kind = "job"; + } + if (metricType === "nginx:status") { + kind = "Ingress" + } const aggregatedMetricsResponse = await api.appMetrics( "", @@ -260,9 +264,9 @@ const MetricsSection: React.FunctionComponent = ({ } const renderShowAutoscalingThresholdsCheckbox = (serviceName: string, isHpaEnabled: boolean) => { - if (serviceName === "") { - return null; - } + if (serviceName === "") { + return null; + } if (!isHpaEnabled) { return null; diff --git a/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/CustomDomains.tsx b/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/CustomDomains.tsx index efb0d380d2e..f6ae48da599 100644 --- a/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/CustomDomains.tsx +++ b/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/CustomDomains.tsx @@ -54,7 +54,6 @@ const CustomDomains: React.FC = ({ index }) => { )} + + + ; + } + + return ( + <> + + { + setSearchValue(x); + }} + placeholder="Search applications . . ." + width="100%" + /> + + , value: "calendar" }, + { label: , value: "letter" }, + ]} + active={sort} + setActive={(x) => { + if (x === "calendar") { + setSort("calendar"); + } else { + setSort("letter"); + } + }} + /> + + + , value: "grid" }, + { label: , value: "list" }, + ]} + active={view} + setActive={(x) => { + if (x === "grid") { + setView("grid"); + } else { + setView("list"); + } + }} + /> + + + + + + + + + + ); + }; + + return ( + + + {renderContents()} + + + ); +}; + +export default Apps; + +const ToggleIcon = styled.img` + height: 12px; + margin: 0 5px; + min-width: 12px; +`; + +const I = styled.i` + color: white; + font-size: 14px; + display: flex; + align-items: center; + margin-right: 5px; + justify-content: center; +`; + +const StyledAppDashboard = styled.div` + width: 100%; + height: 100%; +`; + +const CentralContainer = styled.div` + display: flex; + flex-direction: column; + justify-content: left; + align-items: left; +`; diff --git a/dashboard/src/main/home/app-dashboard/apps/types.ts b/dashboard/src/main/home/app-dashboard/apps/types.ts new file mode 100644 index 00000000000..7723f59f965 --- /dev/null +++ b/dashboard/src/main/home/app-dashboard/apps/types.ts @@ -0,0 +1,12 @@ +import { appRevisionValidator } from "lib/revisions/types"; +import { z } from "zod"; +import { porterAppValidator } from "../app-view/AppView"; + +export const appRevisionWithSourceValidator = z.object({ + app_revision: appRevisionValidator, + source: porterAppValidator, +}); + +export type AppRevisionWithSource = z.infer< + typeof appRevisionWithSourceValidator +>; diff --git a/dashboard/src/shared/api.tsx b/dashboard/src/shared/api.tsx index 6ab50b4fa5b..30f9a94ac0b 100644 --- a/dashboard/src/shared/api.tsx +++ b/dashboard/src/shared/api.tsx @@ -941,6 +941,14 @@ const listAppRevisions = baseApi< return `/api/projects/${project_id}/clusters/${cluster_id}/apps/${porter_app_name}/revisions`; }); +const getLatestAppRevisions = baseApi< +{},{ + project_id: number; + cluster_id: number; +}>("GET", ({ project_id, cluster_id }) => { + return `/api/projects/${project_id}/clusters/${cluster_id}/apps/revisions`; +}) + const getGitlabProcfileContents = baseApi< { path: string; @@ -3043,6 +3051,7 @@ export default { applyApp, getLatestRevision, listAppRevisions, + getLatestAppRevisions, getGitlabProcfileContents, getProjectClusters, getProjectRegistries, From adab64d1a79310d04605a93fc0b820f968bce50f Mon Sep 17 00:00:00 2001 From: Feroze Mohideen Date: Wed, 13 Sep 2023 11:35:12 -0400 Subject: [PATCH 21/59] add telemetry to release middleware (#3561) --- api/server/authz/release.go | 16 ++++++++++------ internal/helm/agent.go | 2 +- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/api/server/authz/release.go b/api/server/authz/release.go index 702aa32529b..0df88d81927 100644 --- a/api/server/authz/release.go +++ b/api/server/authz/release.go @@ -11,6 +11,7 @@ import ( "github.com/porter-dev/porter/api/server/shared/requestutils" "github.com/porter-dev/porter/api/types" "github.com/porter-dev/porter/internal/models" + "github.com/porter-dev/porter/internal/telemetry" "github.com/stefanmcshane/helm/pkg/release" ) @@ -35,22 +36,25 @@ type ReleaseScopedMiddleware struct { } func (p *ReleaseScopedMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) { - cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster) + ctx, span := telemetry.NewSpan(r.Context(), "middleware-release-scope") + defer span.End() - helmAgent, err := p.agentGetter.GetHelmAgent(r.Context(), r, cluster, "") + cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster) + + helmAgent, err := p.agentGetter.GetHelmAgent(ctx, r, cluster, "") if err != nil { - apierrors.HandleAPIError(p.config.Logger, p.config.Alerter, w, r, apierrors.NewErrInternal(err), true) + apierrors.HandleAPIError(p.config.Logger, p.config.Alerter, w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError), true) return } // get the name of the application - reqScopes, _ := r.Context().Value(types.RequestScopeCtxKey).(map[types.PermissionScope]*types.RequestAction) + reqScopes, _ := ctx.Value(types.RequestScopeCtxKey).(map[types.PermissionScope]*types.RequestAction) name := reqScopes[types.ReleaseScope].Resource.Name // get the version for the application version, _ := requestutils.GetURLParamUint(r, types.URLParamReleaseVersion) - release, err := helmAgent.GetRelease(context.Background(), name, int(version), false) + release, err := helmAgent.GetRelease(ctx, name, int(version), false) if err != nil { // ugly casing since at the time of this commit Helm doesn't have an errors package. // so we rely on the Helm error containing "not found" @@ -66,7 +70,7 @@ func (p *ReleaseScopedMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Reque return } - ctx := NewReleaseContext(r.Context(), release) + ctx = NewReleaseContext(ctx, release) r = r.Clone(ctx) p.next.ServeHTTP(w, r) } diff --git a/internal/helm/agent.go b/internal/helm/agent.go index 1db688de13c..a4757896589 100644 --- a/internal/helm/agent.go +++ b/internal/helm/agent.go @@ -111,7 +111,7 @@ func (a *Agent) GetRelease( getDeps bool, ) (*release.Release, error) { ctx, span := telemetry.NewSpan(ctx, "helm-get-release") - // defer span.End() // This span is one of most frequent spans. We need to sample this. For now, this span will not send + defer span.End() // This span is one of most frequent spans. We need to sample this. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "name", Value: name}, From 3c97747edda060b535f79671210af8fe143c31db Mon Sep 17 00:00:00 2001 From: d-g-town <66391417+d-g-town@users.noreply.github.com> Date: Wed, 13 Sep 2023 11:42:06 -0400 Subject: [PATCH 22/59] allow missing porter yaml in CLI (#3558) Co-authored-by: David Townley --- api/client/porter_app.go | 2 + api/server/handlers/porter_app/validate.go | 44 ++++++++++-------- cli/cmd/v2/apply.go | 52 +++++++++++++--------- 3 files changed, 58 insertions(+), 40 deletions(-) diff --git a/api/client/porter_app.go b/api/client/porter_app.go index db2ee77ce27..68913a822ce 100644 --- a/api/client/porter_app.go +++ b/api/client/porter_app.go @@ -180,6 +180,7 @@ func (c *Client) ParseYAML( func (c *Client) ValidatePorterApp( ctx context.Context, projectID, clusterID uint, + appName string, base64AppProto string, deploymentTarget string, commitSHA string, @@ -187,6 +188,7 @@ func (c *Client) ValidatePorterApp( resp := &porter_app.ValidatePorterAppResponse{} req := &porter_app.ValidatePorterAppRequest{ + AppName: appName, Base64AppProto: base64AppProto, DeploymentTargetId: deploymentTarget, CommitSHA: commitSHA, diff --git a/api/server/handlers/porter_app/validate.go b/api/server/handlers/porter_app/validate.go index c40241c84f0..e49f77598b9 100644 --- a/api/server/handlers/porter_app/validate.go +++ b/api/server/handlers/porter_app/validate.go @@ -44,6 +44,7 @@ type Deletions struct { // ValidatePorterAppRequest is the request object for the /apps/validate endpoint type ValidatePorterAppRequest struct { + AppName string `json:"app_name"` Base64AppProto string `json:"b64_app_proto"` DeploymentTargetId string `json:"deployment_target_id"` CommitSHA string `json:"commit_sha"` @@ -81,29 +82,34 @@ func (c *ValidatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ return } - if request.Base64AppProto == "" { - err := telemetry.Error(ctx, span, nil, "b64 yaml is empty") - c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest)) - return - } - - decoded, err := base64.StdEncoding.DecodeString(request.Base64AppProto) - if err != nil { - err := telemetry.Error(ctx, span, err, "error decoding base yaml") - c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest)) - return - } - appProto := &porterv1.PorterApp{} - err = helpers.UnmarshalContractObject(decoded, appProto) - if err != nil { - err := telemetry.Error(ctx, span, err, "error unmarshalling app proto") - c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest)) - return + + if request.Base64AppProto == "" { + if request.AppName == "" { + err := telemetry.Error(ctx, span, nil, "app name is empty and no base64 proto provided") + c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest)) + return + } + + appProto.Name = request.AppName + } else { + decoded, err := base64.StdEncoding.DecodeString(request.Base64AppProto) + if err != nil { + err := telemetry.Error(ctx, span, err, "error decoding base yaml") + c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest)) + return + } + + err = helpers.UnmarshalContractObject(decoded, appProto) + if err != nil { + err := telemetry.Error(ctx, span, err, "error unmarshalling app proto") + c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest)) + return + } } if appProto.Name == "" { - err := telemetry.Error(ctx, span, err, "app proto name is empty") + err := telemetry.Error(ctx, span, nil, "app proto name is empty") c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest)) return } diff --git a/cli/cmd/v2/apply.go b/cli/cmd/v2/apply.go index 41b860289e7..4e26df222a7 100644 --- a/cli/cmd/v2/apply.go +++ b/cli/cmd/v2/apply.go @@ -24,33 +24,41 @@ import ( // Apply implements the functionality of the `porter apply` command for validate apply v2 projects func Apply(ctx context.Context, cliConf config.CLIConfig, client api.Client, porterYamlPath string) error { - if len(porterYamlPath) == 0 { - return fmt.Errorf("porter yaml is empty") + var appName string + if os.Getenv("PORTER_APP_NAME") != "" { + appName = os.Getenv("PORTER_APP_NAME") + } else if os.Getenv("PORTER_STACK_NAME") != "" { + appName = os.Getenv("PORTER_STACK_NAME") } - porterYaml, err := os.ReadFile(filepath.Clean(porterYamlPath)) - if err != nil { - return fmt.Errorf("could not read porter yaml file: %w", err) - } + var yamlB64 string + if len(porterYamlPath) != 0 { + porterYaml, err := os.ReadFile(filepath.Clean(porterYamlPath)) + if err != nil { + return fmt.Errorf("could not read porter yaml file: %w", err) + } - b64YAML := base64.StdEncoding.EncodeToString(porterYaml) + b64YAML := base64.StdEncoding.EncodeToString(porterYaml) - // last argument is passed to accommodate users with v1 porter yamls - parseResp, err := client.ParseYAML(ctx, cliConf.Project, cliConf.Cluster, b64YAML, os.Getenv("PORTER_STACK_NAME")) - if err != nil { - return fmt.Errorf("error calling parse yaml endpoint: %w", err) - } + // last argument is passed to accommodate users with v1 porter yamls + parseResp, err := client.ParseYAML(ctx, cliConf.Project, cliConf.Cluster, b64YAML, os.Getenv("PORTER_STACK_NAME")) + if err != nil { + return fmt.Errorf("error calling parse yaml endpoint: %w", err) + } - if parseResp.B64AppProto == "" { - return errors.New("b64 app proto is empty") - } + if parseResp.B64AppProto == "" { + return errors.New("b64 app proto is empty") + } + yamlB64 = parseResp.B64AppProto - appName, err := appNameFromB64AppProto(parseResp.B64AppProto) - if err != nil { - return fmt.Errorf("error getting app name from b64 app proto: %w", err) - } + // override app name if provided + appName, err = appNameFromB64AppProto(parseResp.B64AppProto) + if err != nil { + return fmt.Errorf("error getting app name from b64 app proto: %w", err) + } - color.New(color.FgGreen).Printf("Successfully parsed Porter YAML: applying app \"%s\"\n", appName) // nolint:errcheck,gosec + color.New(color.FgGreen).Printf("Successfully parsed Porter YAML: applying app \"%s\"\n", appName) // nolint:errcheck,gosec + } targetResp, err := client.DefaultDeploymentTarget(ctx, cliConf.Project, cliConf.Cluster) if err != nil { @@ -70,7 +78,7 @@ func Apply(ctx context.Context, cliConf config.CLIConfig, client api.Client, por commitSHA = commit.Sha } - validateResp, err := client.ValidatePorterApp(ctx, cliConf.Project, cliConf.Cluster, parseResp.B64AppProto, targetResp.DeploymentTargetID, commitSHA) + validateResp, err := client.ValidatePorterApp(ctx, cliConf.Project, cliConf.Cluster, appName, yamlB64, targetResp.DeploymentTargetID, commitSHA) if err != nil { return fmt.Errorf("error calling validate endpoint: %w", err) } @@ -154,6 +162,8 @@ func Apply(ctx context.Context, cliConf config.CLIConfig, client api.Client, por } } + color.New(color.FgGreen).Printf("Image tag exists in repository") // nolint:errcheck,gosec + if applyResp.CLIAction == porterv1.EnumCLIAction_ENUM_CLI_ACTION_TRACK_PREDEPLOY { color.New(color.FgGreen).Printf("Waiting for predeploy to complete...\n") // nolint:errcheck,gosec From c584f669bc7fe2271c4f74ae2d49543e79113bbd Mon Sep 17 00:00:00 2001 From: Feroze Mohideen Date: Wed, 13 Sep 2023 11:50:58 -0400 Subject: [PATCH 23/59] Add retry to get release request (#3562) --- api/client/api.go | 53 ++++++++++++++++++++++++++++++++++++++++------- api/client/k8s.go | 1 + 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/api/client/api.go b/api/client/api.go index fb388f03bdb..b8625463c9e 100644 --- a/api/client/api.go +++ b/api/client/api.go @@ -80,9 +80,36 @@ func NewClientWithConfig(ctx context.Context, input NewClientInput) (Client, err // ErrNoAuthCredential returns an error when no auth credentials have been provided such as cookies or tokens var ErrNoAuthCredential = errors.New("unable to create an API session with cookie nor token") -func (c *Client) getRequest(relPath string, data interface{}, response interface{}) error { +// getRequestConfig defines configuration for a GET request +type getRequestConfig struct { + retryCount uint +} + +// withRetryCount is a convenience function for setting the retry count +func withRetryCount(retryCount uint) func(*getRequestConfig) { + return func(o *getRequestConfig) { + o.retryCount = retryCount + } +} + +// getRequest is responsible for making a GET request to the API +func (c *Client) getRequest(relPath string, data interface{}, response interface{}, opts ...func(*getRequestConfig)) error { + config := &getRequestConfig{ + retryCount: 1, + } + + for _, opt := range opts { + opt(config) + } + + var httpErr *types.ExternalError + var err error + vals := make(map[string][]string) - err := schema.NewEncoder().Encode(data, vals) + err = schema.NewEncoder().Encode(data, vals) + if err != nil { + return err + } urlVals := url.Values(vals) encodedURLVals := urlVals.Encode() @@ -106,15 +133,27 @@ func (c *Client) getRequest(relPath string, data interface{}, response interface return err } - if httpErr, err := c.sendRequest(req, response, true); httpErr != nil || err != nil { - if httpErr != nil { - return fmt.Errorf("%v", httpErr.Error) + for i := 0; i < int(config.retryCount); i++ { + httpErr, err = c.sendRequest(req, response, true) + + if httpErr == nil && err == nil { + return nil } - return err + if i != int(config.retryCount)-1 { + if httpErr != nil { + fmt.Fprintf(os.Stderr, "Error: %s (status code %d), retrying request...\n", httpErr.Error, httpErr.Code) + } else { + fmt.Fprintf(os.Stderr, "Error: %v, retrying request...\n", err) + } + } } - return nil + if httpErr != nil { + return fmt.Errorf("%v", httpErr.Error) + } + + return err } type postRequestOpts struct { diff --git a/api/client/k8s.go b/api/client/k8s.go index bf50cb26736..67f32a00fc9 100644 --- a/api/client/k8s.go +++ b/api/client/k8s.go @@ -182,6 +182,7 @@ func (c *Client) GetRelease( ), nil, resp, + withRetryCount(3), ) return resp, err From 0f2df24f9959b3973ec49f94a0a250551323bb10 Mon Sep 17 00:00:00 2001 From: Feroze Mohideen Date: Wed, 13 Sep 2023 12:38:45 -0400 Subject: [PATCH 24/59] [towards POR-1668] Fix pre-deploy logs in activity feed (#3564) --- .../handlers/porter_app/logs_apply_v2.go | 12 +- api/server/handlers/porter_app/stream_logs.go | 4 + dashboard/src/lib/porter-apps/services.ts | 3 +- .../app-dashboard/app-view/tabs/LogsTab.tsx | 3 +- .../events/cards/PreDeployEventCard.tsx | 2 +- .../focus-views/PredeployEventFocusView.tsx | 11 +- .../tabs/activity-feed/events/types.ts | 1 + .../validate-apply/logs/Logs.tsx | 25 +-- .../validate-apply/logs/utils.ts | 40 ++--- dashboard/src/shared/api.tsx | 152 ++++++++---------- 10 files changed, 131 insertions(+), 122 deletions(-) diff --git a/api/server/handlers/porter_app/logs_apply_v2.go b/api/server/handlers/porter_app/logs_apply_v2.go index 048d51a6717..4f6e63ea7d2 100644 --- a/api/server/handlers/porter_app/logs_apply_v2.go +++ b/api/server/handlers/porter_app/logs_apply_v2.go @@ -48,12 +48,14 @@ type AppLogsRequest struct { EndRange time.Time `schema:"end_range,omitempty"` SearchParam string `schema:"search_param"` Direction string `schema:"direction"` + AppRevisionID string `schema:"app_revision_id"` } const ( - lokiLabel_PorterAppName = "porter_run_app_name" - lokiLabel_PorterServiceName = "porter_run_service_name" - lokiLabel_Namespace = "namespace" + lokiLabel_PorterAppName = "porter_run_app_name" + lokiLabel_PorterServiceName = "porter_run_service_name" + lokiLabel_PorterAppRevisionID = "porter_run_app_revision_id" + lokiLabel_Namespace = "namespace" ) // ServeHTTP gets logs for a given app, service, and deployment target @@ -152,6 +154,10 @@ func (c *AppLogsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { matchLabels[lokiLabel_PorterServiceName] = request.ServiceName } + if request.AppRevisionID != "" { + matchLabels[lokiLabel_PorterAppRevisionID] = request.AppRevisionID + } + logRequest := &types.LogRequest{ Limit: request.Limit, StartRange: &request.StartRange, diff --git a/api/server/handlers/porter_app/stream_logs.go b/api/server/handlers/porter_app/stream_logs.go index 8deb3286e5a..03dc5c4f117 100644 --- a/api/server/handlers/porter_app/stream_logs.go +++ b/api/server/handlers/porter_app/stream_logs.go @@ -133,6 +133,10 @@ func (c *StreamLogsLokiHandler) ServeHTTP(w http.ResponseWriter, r *http.Request labels = append(labels, fmt.Sprintf("%s=%s", lokiLabel_PorterServiceName, request.ServiceName)) } + if request.AppRevisionID != "" { + labels = append(labels, fmt.Sprintf("%s=%s", lokiLabel_PorterAppRevisionID, request.AppRevisionID)) + } + telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "labels", Value: strings.Join(labels, ",")}) err = agent.StreamPorterAgentLokiLog(labels, string(startTime), request.SearchParam, 0, safeRW) diff --git a/dashboard/src/lib/porter-apps/services.ts b/dashboard/src/lib/porter-apps/services.ts index dd232deff27..33964e46811 100644 --- a/dashboard/src/lib/porter-apps/services.ts +++ b/dashboard/src/lib/porter-apps/services.ts @@ -285,7 +285,8 @@ export function deserializeService({ health: config.healthCheck, override: overrideWebConfig?.healthCheck, }), - domains: [...config.domains, ...(overrideWebConfig?.domains ?? [])].map((domain) => ({ + + domains: Array.from(new Set([...config.domains, ...(overrideWebConfig?.domains ?? [])])).map((domain) => ({ name: ServiceField.string( domain.name, overrideWebConfig?.domains.find( diff --git a/dashboard/src/main/home/app-dashboard/app-view/tabs/LogsTab.tsx b/dashboard/src/main/home/app-dashboard/app-view/tabs/LogsTab.tsx index b662eeb9e02..9cca803d1d4 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/tabs/LogsTab.tsx +++ b/dashboard/src/main/home/app-dashboard/app-view/tabs/LogsTab.tsx @@ -3,7 +3,7 @@ import Logs from "../../validate-apply/logs/Logs" import { useLatestRevision } from "../LatestRevisionContext"; const LogsTab: React.FC = () => { - const { projectId, clusterId, latestProto, deploymentTargetId, latestRevision } = useLatestRevision(); + const { projectId, clusterId, latestProto, deploymentTargetId } = useLatestRevision(); const appName = latestProto.name const serviceNames = Object.keys(latestProto.services) @@ -15,7 +15,6 @@ const LogsTab: React.FC = () => { appName={appName} serviceNames={serviceNames} deploymentTargetId={deploymentTargetId} - latestRevision={latestRevision} /> ); }; diff --git a/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/PreDeployEventCard.tsx b/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/PreDeployEventCard.tsx index a86eacd3f62..2e33b5aa366 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/PreDeployEventCard.tsx +++ b/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/PreDeployEventCard.tsx @@ -63,7 +63,7 @@ const PreDeployEventCard: React.FC = ({ event, appName, projectId, cluste <> - + diff --git a/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/focus-views/PredeployEventFocusView.tsx b/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/focus-views/PredeployEventFocusView.tsx index dc2c3cfd877..4b23473a806 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/focus-views/PredeployEventFocusView.tsx +++ b/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/focus-views/PredeployEventFocusView.tsx @@ -3,7 +3,7 @@ import React from "react"; import dayjs from "dayjs"; import Text from "components/porter/Text"; import { readableDate } from "shared/string_utils"; -import { getDuration } from "../utils"; +import { getDuration, getStatusColor } from "../utils"; import { AppearingView } from "./EventFocusView"; import Icon from "components/porter/Icon"; import loading from "assets/loading.gif"; @@ -27,15 +27,15 @@ const PreDeployEventFocusView: React.FC = ({ const renderHeaderText = () => { switch (event.status) { case "SUCCESS": - return Pre-deploy succeeded; + return Pre-deploy succeeded; case "FAILED": - return Pre-deploy failed; + return Pre-deploy failed; default: return ( - Pre-deploy in progress... + Pre-deploy in progress... ); } @@ -64,7 +64,8 @@ const PreDeployEventFocusView: React.FC = ({ appName={appName} serviceNames={serviceNames} deploymentTargetId={deploymentTargetId} - latestRevision={latestRevision} + appRevisionId={event.metadata.app_revision_id} + logFilterNames={["service_name"]} /> ); diff --git a/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/types.ts b/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/types.ts index 431b79f3dd8..bcb09243b6a 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/types.ts +++ b/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/types.ts @@ -30,6 +30,7 @@ const porterAppBuildEventMetadataValidator = z.object({ const porterAppPreDeployEventMetadataValidator = z.object({ start_time: z.string(), end_time: z.string().optional(), + app_revision_id: z.string(), }); export const porterAppEventValidator = z.discriminatedUnion("type", [ z.object({ diff --git a/dashboard/src/main/home/app-dashboard/validate-apply/logs/Logs.tsx b/dashboard/src/main/home/app-dashboard/validate-apply/logs/Logs.tsx index 4d8381466ef..ec522ff4ead 100644 --- a/dashboard/src/main/home/app-dashboard/validate-apply/logs/Logs.tsx +++ b/dashboard/src/main/home/app-dashboard/validate-apply/logs/Logs.tsx @@ -28,6 +28,7 @@ import StyledLogs from "../../expanded-app/logs/StyledLogs"; import { AppRevision } from "lib/revisions/types"; import { useLatestRevisionNumber, useRevisionIdToNumber } from "lib/hooks/useRevisionList"; import { useLocation } from "react-router"; +import { valueExists } from "shared/util"; type Props = { projectId: number; @@ -35,7 +36,8 @@ type Props = { appName: string; serviceNames: string[]; deploymentTargetId: string; - latestRevision: AppRevision; + appRevisionId?: string; + logFilterNames?: LogFilterName[]; }; const Logs: React.FC = ({ @@ -44,7 +46,8 @@ const Logs: React.FC = ({ appName, serviceNames, deploymentTargetId, - latestRevision, + appRevisionId, + logFilterNames = ["service_name", "revision", "output_stream"], }) => { const { search } = useLocation(); const queryParams = new URLSearchParams(search); @@ -131,7 +134,7 @@ const Logs: React.FC = ({ service_name: value, })); } - }, + } as GenericLogFilter, { name: "revision", displayName: "Version", @@ -143,7 +146,7 @@ const Logs: React.FC = ({ revision: value, })); } - }, + } as GenericLogFilter, { name: "output_stream", displayName: "Output Stream", @@ -158,8 +161,9 @@ const Logs: React.FC = ({ output_stream: value, })); } - }, - ]); + } as GenericLogFilter, + ].filter((f: GenericLogFilter) => logFilterNames.includes(f.name))); + const notify = (message: string) => { setNotification(message); @@ -181,6 +185,7 @@ const Logs: React.FC = ({ setIsLoading, revisionIdToNumber, selectedDate, + appRevisionId, ); useEffect(() => { @@ -198,7 +203,7 @@ const Logs: React.FC = ({ service_name: value, })); } - }, + } as GenericLogFilter, { name: "revision", displayName: "Version", @@ -210,7 +215,7 @@ const Logs: React.FC = ({ revision: value, })); } - }, + } as GenericLogFilter, { name: "output_stream", displayName: "Output Stream", @@ -225,8 +230,8 @@ const Logs: React.FC = ({ output_stream: value, })); } - }, - ]) + } as GenericLogFilter, + ].filter((f: GenericLogFilter) => logFilterNames.includes(f.name))) }, [latestRevisionNumber]); useEffect(() => { diff --git a/dashboard/src/main/home/app-dashboard/validate-apply/logs/utils.ts b/dashboard/src/main/home/app-dashboard/validate-apply/logs/utils.ts index 5148c707bd4..4822c9afee6 100644 --- a/dashboard/src/main/home/app-dashboard/validate-apply/logs/utils.ts +++ b/dashboard/src/main/home/app-dashboard/validate-apply/logs/utils.ts @@ -42,8 +42,8 @@ export const parseLogs = (logs: any[] = []): PorterLog[] => { }; export const useLogs = ( - projectID: number, - clusterID: number, + projectID: number, + clusterID: number, selectedFilterValues: Record, appName: string, serviceName: string, @@ -52,8 +52,9 @@ export const useLogs = ( notify: (message: string) => void, setLoading: (isLoading: boolean) => void, revisionIdToNumber: Record, - // if setDate is set, results are not live + // if setDate is set, results are not live setDate?: Date, + appRevisionId: string = "", timeRange?: { startTime?: Dayjs, endTime?: Dayjs, @@ -166,6 +167,7 @@ export const useLogs = ( service_name: serviceName, deployment_target_id: deploymentTargetId, search_param: searchParam, + app_revision_id: appRevisionId, } const q = new URLSearchParams(searchParams).toString(); @@ -212,12 +214,12 @@ export const useLogs = ( } if (selectedFilterValues.output_stream !== GenericLogFilter.getDefaultOption("output_stream").value && - log.metadata.output_stream !== selectedFilterValues.output_stream) { + log.metadata.output_stream !== selectedFilterValues.output_stream) { return false; } if (selectedFilterValues.revision !== GenericLogFilter.getDefaultOption("revision").value && - log.metadata.revision !== selectedFilterValues.revision) { + log.metadata.revision !== selectedFilterValues.revision) { return false; } @@ -245,15 +247,16 @@ export const useLogs = ( end_range: endDate, limit, direction, + app_revision_id: appRevisionId, }; const logsResp = await api.appLogs( - "", - getLogsReq, - { - cluster_id: clusterID, - project_id: projectID, - } + "", + getLogsReq, + { + cluster_id: clusterID, + project_id: projectID, + } ) if (logsResp.data == null) { @@ -271,15 +274,16 @@ export const useLogs = ( newLogs.filter((log) => { return log.metadata?.raw_labels?.porter_run_app_revision_id != null - && revisionIdToNumber[log.metadata.raw_labels.porter_run_app_revision_id] != null - && revisionIdToNumber[log.metadata.raw_labels.porter_run_app_revision_id] != 0 + && revisionIdToNumber[log.metadata.raw_labels.porter_run_app_revision_id] != null + && revisionIdToNumber[log.metadata.raw_labels.porter_run_app_revision_id] != 0 }).forEach((log) => { if (log.metadata?.raw_labels?.porter_run_app_revision_id != null) { - const revisionNumber = revisionIdToNumber[log.metadata.raw_labels.porter_run_app_revision_id]; - if (revisionNumber != null && revisionNumber != 0) { - log.metadata.revision = revisionNumber.toString(); - } - }}) + const revisionNumber = revisionIdToNumber[log.metadata.raw_labels.porter_run_app_revision_id]; + if (revisionNumber != null && revisionNumber != 0) { + log.metadata.revision = revisionNumber.toString(); + } + } + }) return { logs: newLogs, diff --git a/dashboard/src/shared/api.tsx b/dashboard/src/shared/api.tsx index 30f9a94ac0b..5bd74fd7cd9 100644 --- a/dashboard/src/shared/api.tsx +++ b/dashboard/src/shared/api.tsx @@ -279,24 +279,25 @@ const getLogsWithinTimeRange = baseApi< ); const appLogs = baseApi< - { - app_name: string; - service_name: string; - deployment_target_id: string; - limit: number; - start_range: string; - end_range: string; - search_param?: string; - direction?: string; - }, - { - project_id: number; - cluster_id: number; - } + { + app_name: string; + service_name: string; + deployment_target_id: string; + limit: number; + start_range: string; + end_range: string; + search_param?: string; + direction?: string; + app_revision_id?: string; + }, + { + project_id: number; + cluster_id: number; + } >( - "GET", - ({ project_id, cluster_id }) => - `/api/projects/${project_id}/clusters/${cluster_id}/apps/logs` + "GET", + ({ project_id, cluster_id }) => + `/api/projects/${project_id}/clusters/${cluster_id}/apps/logs` ); const getFeedEvents = baseApi< @@ -309,9 +310,8 @@ const getFeedEvents = baseApi< } >("GET", (pathParams) => { let { project_id, cluster_id, stack_name, page } = pathParams; - return `/api/projects/${project_id}/clusters/${cluster_id}/applications/${stack_name}/events?page=${ - page || 1 - }`; + return `/api/projects/${project_id}/clusters/${cluster_id}/applications/${stack_name}/events?page=${page || 1 + }`; }); const createEnvironment = baseApi< @@ -736,11 +736,9 @@ const detectBuildpack = baseApi< 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)}/buildpack/detect`; + return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id + }/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name + }/${encodeURIComponent(pathParams.branch)}/buildpack/detect`; }); const detectGitlabBuildpack = baseApi< @@ -771,11 +769,9 @@ const getBranchContents = baseApi< 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)}/contents`; + return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id + }/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name + }/${encodeURIComponent(pathParams.branch)}/contents`; }); const getProcfileContents = baseApi< @@ -791,11 +787,9 @@ const getProcfileContents = baseApi< 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)}/procfile`; + return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id + }/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name + }/${encodeURIComponent(pathParams.branch)}/procfile`; }); const getPorterYamlContents = baseApi< @@ -811,11 +805,9 @@ const getPorterYamlContents = baseApi< 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)}/porteryaml`; + return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id + }/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name + }/${encodeURIComponent(pathParams.branch)}/porteryaml`; }); const parsePorterYaml = baseApi< @@ -851,11 +843,9 @@ const getBranchHead = baseApi< 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`; + 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< @@ -878,21 +868,21 @@ const validatePorterApp = baseApi< const createApp = baseApi< | { - name: string; - type: "github"; - git_repo_id: number; - git_branch: string; - git_repo_name: string; - porter_yaml_path: string; - } + name: string; + type: "github"; + git_repo_id: number; + git_branch: string; + git_repo_name: string; + porter_yaml_path: string; + } | { - name: string; - type: "docker-registry"; - image: { - repository: string; - tag: string; - }; - }, + name: string; + type: "docker-registry"; + image: { + repository: string; + tag: string; + }; + }, { project_id: number; cluster_id: number; @@ -1485,24 +1475,24 @@ const getMetrics = baseApi< }); const appMetrics = baseApi< - { - metric: string; - shouldsum: boolean; - pods?: string[]; - kind?: string; // the controller kind - name?: string; - percentile?: number; - deployment_target_id: string; - startrange: number; - endrange: number; - resolution: string; - }, - { - id: number; - cluster_id: number; - } + { + metric: string; + shouldsum: boolean; + pods?: string[]; + kind?: string; // the controller kind + name?: string; + percentile?: number; + deployment_target_id: string; + startrange: number; + endrange: number; + resolution: string; + }, + { + id: number; + cluster_id: number; + } >("GET", (pathParams) => { - return `/api/projects/${pathParams.id}/clusters/${pathParams.cluster_id}/apps/metrics`; + return `/api/projects/${pathParams.id}/clusters/${pathParams.cluster_id}/apps/metrics`; }); const getNamespaces = baseApi< @@ -1836,11 +1826,9 @@ const getEnvGroup = baseApi< version?: number; } >("GET", (pathParams) => { - return `/api/projects/${pathParams.id}/clusters/${ - pathParams.cluster_id - }/namespaces/${pathParams.namespace}/envgroup?name=${pathParams.name}${ - pathParams.version ? "&version=" + pathParams.version : "" - }`; + return `/api/projects/${pathParams.id}/clusters/${pathParams.cluster_id + }/namespaces/${pathParams.namespace}/envgroup?name=${pathParams.name}${pathParams.version ? "&version=" + pathParams.version : "" + }`; }); const getConfigMap = baseApi< @@ -2897,7 +2885,7 @@ const removeStackEnvGroup = baseApi< `/api/v1/projects/${project_id}/clusters/${cluster_id}/namespaces/${namespace}/stacks/${stack_id}/remove_env_group/${env_group_name}` ); -const getGithubStatus = baseApi<{}, {}>("GET", ({}) => `/api/status/github`); +const getGithubStatus = baseApi<{}, {}>("GET", ({ }) => `/api/status/github`); const createSecretAndOpenGitHubPullRequest = baseApi< { From 75185b58f570cda48316682c1c29a44787d960a6 Mon Sep 17 00:00:00 2001 From: ianedwards Date: Wed, 13 Sep 2023 13:34:05 -0400 Subject: [PATCH 25/59] POR-1708 route update full to v2 apply (#3563) --- cli/cmd/commands/apply.go | 21 ++++++++++++--------- cli/cmd/commands/update.go | 2 +- cli/cmd/v2/apply.go | 13 +++---------- cli/cmd/v2/deploy.go | 14 ++++++++++++-- 4 files changed, 28 insertions(+), 22 deletions(-) diff --git a/cli/cmd/commands/apply.go b/cli/cmd/commands/apply.go index c6c69fb14de..5c27a2ebba1 100644 --- a/cli/cmd/commands/apply.go +++ b/cli/cmd/commands/apply.go @@ -114,8 +114,15 @@ func apply(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client ap return fmt.Errorf("could not retrieve project from Porter API. Please contact support@porter.run") } + var appName string + if os.Getenv("PORTER_APP_NAME") != "" { + appName = os.Getenv("PORTER_APP_NAME") + } else if os.Getenv("PORTER_STACK_NAME") != "" { + appName = os.Getenv("PORTER_STACK_NAME") + } + if project.ValidateApplyV2 { - err = v2.Apply(ctx, cliConfig, client, porterYAML) + err = v2.Apply(ctx, cliConfig, client, porterYAML, appName) if err != nil { return err } @@ -123,11 +130,8 @@ func apply(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client ap } fileBytes, err := os.ReadFile(porterYAML) //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error - if err != nil { - stackName := os.Getenv("PORTER_STACK_NAME") - if stackName == "" { - return fmt.Errorf("a valid porter.yaml file must be specified. Run porter apply --help for more information") - } + if err != nil && appName == "" { + return fmt.Errorf("a valid porter.yaml file must be specified. Run porter apply --help for more information") } var previewVersion struct { @@ -187,8 +191,8 @@ func apply(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client ap } if parsed.Applications != nil { - for appName, app := range parsed.Applications { - resources, err := porter_app.CreateApplicationDeploy(ctx, client, worker, app, appName, cliConfig) + for name, app := range parsed.Applications { + resources, err := porter_app.CreateApplicationDeploy(ctx, client, worker, app, name, cliConfig) if err != nil { return fmt.Errorf("error parsing porter.yaml for build resources: %w", err) } @@ -196,7 +200,6 @@ func apply(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client ap resGroup.Resources = append(resGroup.Resources, resources...) } } else { - appName := os.Getenv("PORTER_STACK_NAME") if appName == "" { return fmt.Errorf("environment variable PORTER_STACK_NAME must be set") } diff --git a/cli/cmd/commands/update.go b/cli/cmd/commands/update.go index 174ee993591..d7eecbb17d5 100644 --- a/cli/cmd/commands/update.go +++ b/cli/cmd/commands/update.go @@ -445,7 +445,7 @@ the image that the application uses if no --values file is specified: func updateFull(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, args []string) error { if featureFlags.ValidateApplyV2Enabled { - err := v2.UpdateFull(ctx) + err := v2.UpdateFull(ctx, cliConf, client, app) if err != nil { return err } diff --git a/cli/cmd/v2/apply.go b/cli/cmd/v2/apply.go index 4e26df222a7..62455476d1b 100644 --- a/cli/cmd/v2/apply.go +++ b/cli/cmd/v2/apply.go @@ -23,14 +23,7 @@ import ( ) // Apply implements the functionality of the `porter apply` command for validate apply v2 projects -func Apply(ctx context.Context, cliConf config.CLIConfig, client api.Client, porterYamlPath string) error { - var appName string - if os.Getenv("PORTER_APP_NAME") != "" { - appName = os.Getenv("PORTER_APP_NAME") - } else if os.Getenv("PORTER_STACK_NAME") != "" { - appName = os.Getenv("PORTER_STACK_NAME") - } - +func Apply(ctx context.Context, cliConf config.CLIConfig, client api.Client, porterYamlPath string, appName string) error { var yamlB64 string if len(porterYamlPath) != 0 { porterYaml, err := os.ReadFile(filepath.Clean(porterYamlPath)) @@ -41,7 +34,7 @@ func Apply(ctx context.Context, cliConf config.CLIConfig, client api.Client, por b64YAML := base64.StdEncoding.EncodeToString(porterYaml) // last argument is passed to accommodate users with v1 porter yamls - parseResp, err := client.ParseYAML(ctx, cliConf.Project, cliConf.Cluster, b64YAML, os.Getenv("PORTER_STACK_NAME")) + parseResp, err := client.ParseYAML(ctx, cliConf.Project, cliConf.Cluster, b64YAML, appName) if err != nil { return fmt.Errorf("error calling parse yaml endpoint: %w", err) } @@ -162,7 +155,7 @@ func Apply(ctx context.Context, cliConf config.CLIConfig, client api.Client, por } } - color.New(color.FgGreen).Printf("Image tag exists in repository") // nolint:errcheck,gosec + color.New(color.FgGreen).Printf("Image tag exists in repository\n") // nolint:errcheck,gosec if applyResp.CLIAction == porterv1.EnumCLIAction_ENUM_CLI_ACTION_TRACK_PREDEPLOY { color.New(color.FgGreen).Printf("Waiting for predeploy to complete...\n") // nolint:errcheck,gosec diff --git a/cli/cmd/v2/deploy.go b/cli/cmd/v2/deploy.go index 0901ae31903..f67fff813ba 100644 --- a/cli/cmd/v2/deploy.go +++ b/cli/cmd/v2/deploy.go @@ -3,11 +3,21 @@ package v2 import ( "context" "fmt" + + api "github.com/porter-dev/porter/api/client" + "github.com/porter-dev/porter/cli/cmd/config" ) // UpdateFull implements the functionality of the `porter build` command for validate apply v2 projects -func UpdateFull(ctx context.Context) error { - fmt.Println("This command is not supported for your project. Contact support@porter.run for more information.") +func UpdateFull(ctx context.Context, cliConf config.CLIConfig, client api.Client, appName string) error { + // use empty string for porterYamlPath,legacy projects wont't have a v2 porter.yaml + var porterYamlPath string + + err := Apply(ctx, cliConf, client, porterYamlPath, appName) + if err != nil { + return err + } + return nil } From f67f40c62035efd80327c01210391ded2b6e8610 Mon Sep 17 00:00:00 2001 From: ianedwards Date: Wed, 13 Sep 2023 15:06:53 -0400 Subject: [PATCH 26/59] update validator (#3565) --- dashboard/src/main/home/app-dashboard/app-view/AppView.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dashboard/src/main/home/app-dashboard/app-view/AppView.tsx b/dashboard/src/main/home/app-dashboard/app-view/AppView.tsx index 0009216e364..e21d853a86d 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/AppView.tsx +++ b/dashboard/src/main/home/app-dashboard/app-view/AppView.tsx @@ -8,7 +8,6 @@ import Spacer from "components/porter/Spacer"; import AppDataContainer from "./AppDataContainer"; -import web from "assets/web.png"; import AppHeader from "./AppHeader"; import { LatestRevisionProvider } from "./LatestRevisionContext"; @@ -19,7 +18,10 @@ export const porterAppValidator = z.object({ repo_name: z.string().optional(), build_context: z.string().optional(), builder: z.string().optional(), - buildpacks: z.array(z.string()).optional(), + buildpacks: z + .string() + .transform((s) => s.split(",")) + .optional(), dockerfile: z.string().optional(), image_repo_uri: z.string().optional(), porter_yaml_path: z.string().optional(), From dd6d0e12d093bc70cc9482065979eb3995449fc7 Mon Sep 17 00:00:00 2001 From: ianedwards Date: Wed, 13 Sep 2023 16:26:43 -0400 Subject: [PATCH 27/59] POR-1706 show loading state when deleting app (#3567) --- .../app-dashboard/app-view/tabs/Settings.tsx | 8 +- .../expanded-app/DeleteApplicationModal.tsx | 95 ++++++++++--------- 2 files changed, 59 insertions(+), 44 deletions(-) diff --git a/dashboard/src/main/home/app-dashboard/app-view/tabs/Settings.tsx b/dashboard/src/main/home/app-dashboard/app-view/tabs/Settings.tsx index f2be491d4d4..ceb1985f405 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/tabs/Settings.tsx +++ b/dashboard/src/main/home/app-dashboard/app-view/tabs/Settings.tsx @@ -18,6 +18,7 @@ const Settings: React.FC = () => { const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const { porterApp, clusterId, projectId } = useLatestRevision(); const { updateAppStep } = useAppAnalytics(porterApp.name); + const [isDeleting, setIsDeleting] = useState(false); const githubWorkflowFilename = `porter_stack_${porterApp.name}.yml`; @@ -55,6 +56,7 @@ const Settings: React.FC = () => { const onDelete = useCallback( async (deleteWorkflow?: boolean) => { try { + setIsDeleting(true); await api.deletePorterApp( "", {}, @@ -103,7 +105,10 @@ const Settings: React.FC = () => { updateAppStep({ step: "stack-deletion", deleteWorkflow: false }); history.push("/apps"); - } catch (err) {} + } catch (err) { + } finally { + setIsDeleting(false); + } }, [githubWorkflowFilename, porterApp.name, clusterId, projectId] ); @@ -130,6 +135,7 @@ const Settings: React.FC = () => { closeModal={() => setIsDeleteModalOpen(false)} githubWorkflowFilename={githubWorkflowFilename} deleteApplication={onDelete} + loading={isDeleting} /> )} diff --git a/dashboard/src/main/home/app-dashboard/expanded-app/DeleteApplicationModal.tsx b/dashboard/src/main/home/app-dashboard/expanded-app/DeleteApplicationModal.tsx index ae72d73d43b..44d628d02ae 100644 --- a/dashboard/src/main/home/app-dashboard/expanded-app/DeleteApplicationModal.tsx +++ b/dashboard/src/main/home/app-dashboard/expanded-app/DeleteApplicationModal.tsx @@ -8,60 +8,69 @@ import React, { useState } from "react"; import styled from "styled-components"; type Props = { - closeModal: () => void; - githubWorkflowFilename: string; - deleteApplication: (deleteWorkflowFile?: boolean) => void; + closeModal: () => void; + githubWorkflowFilename: string; + deleteApplication: (deleteWorkflowFile?: boolean) => void; + loading?: boolean; }; const GithubActionModal: React.FC = ({ - closeModal, - githubWorkflowFilename, - deleteApplication, + closeModal, + githubWorkflowFilename, + deleteApplication, + loading = false, }) => { - const [deleteGithubWorkflow, setDeleteGithubWorkflow] = useState(true); + const [deleteGithubWorkflow, setDeleteGithubWorkflow] = useState(true); - const renderDeleteGithubWorkflowText = () => { - if (githubWorkflowFilename === "") { - return null; - } - return ( - <> - You may also want to remove this application's associated CI file from your repository. - - setDeleteGithubWorkflow(!deleteGithubWorkflow)} - > - - Upon deletion, open a PR to remove this application's associated CI file ({githubWorkflowFilename}) from my repository. - - - - - ) + const renderDeleteGithubWorkflowText = () => { + if (githubWorkflowFilename === "") { + return null; } - return ( - - - Confirm deletion - - - Click the button below to confirm deletion. This action is irreversible. - - {renderDeleteGithubWorkflowText()} - - + <> + + You may also want to remove this application's associated CI file from + your repository. + + + setDeleteGithubWorkflow(!deleteGithubWorkflow)} + > + + Upon deletion, open a PR to remove this application's associated CI + file ({githubWorkflowFilename}) from my repository. + + + + ); + }; + + return ( + + Confirm deletion + + + Click the button below to confirm deletion. This action is irreversible. + + + {renderDeleteGithubWorkflowText()} + + + ); }; export default GithubActionModal; const Code = styled.span` font-family: monospace; -`; \ No newline at end of file +`; From 94665da8490419c8d04412af20ea0c33337a4626 Mon Sep 17 00:00:00 2001 From: sunguroku <65516095+sunguroku@users.noreply.github.com> Date: Wed, 13 Sep 2023 16:38:54 -0400 Subject: [PATCH 28/59] GTM (#3568) --- .../src/components/AzureProvisionerSettings.tsx | 13 +++++++++++++ dashboard/src/components/CloudFormationForm.tsx | 16 ++++++++++++++-- .../src/components/GCPProvisionerSettings.tsx | 14 ++++++++++++++ dashboard/src/index.html | 3 +++ dashboard/src/main/auth/Login.tsx | 2 +- dashboard/src/main/auth/Register.tsx | 14 +++++++++++++- dashboard/src/main/auth/SetInfo.tsx | 14 +++++++++++++- 7 files changed, 71 insertions(+), 5 deletions(-) diff --git a/dashboard/src/components/AzureProvisionerSettings.tsx b/dashboard/src/components/AzureProvisionerSettings.tsx index 6bc46e97456..e454d8afa44 100644 --- a/dashboard/src/components/AzureProvisionerSettings.tsx +++ b/dashboard/src/components/AzureProvisionerSettings.tsx @@ -142,6 +142,19 @@ const AzureProvisionerSettings: React.FC = (props) => { } setIsClicked(true); + + try { + window.dataLayer?.push({ + event: 'provision-attempt', + data: { + cloud: 'azure', + email: user?.email + } + }); + } catch (err) { + console.log(err); + } + var data = new Contract({ cluster: new Cluster({ projectId: currentProject.id, diff --git a/dashboard/src/components/CloudFormationForm.tsx b/dashboard/src/components/CloudFormationForm.tsx index bb7dadc9c45..116a0dc37e7 100644 --- a/dashboard/src/components/CloudFormationForm.tsx +++ b/dashboard/src/components/CloudFormationForm.tsx @@ -66,7 +66,7 @@ const CloudFormationForm: React.FC = ({ const [hasClickedCloudformationButton, setHasClickedCloudformationButton] = useState(false); const [showNeedHelpModal, setShowNeedHelpModal] = useState(false); - const { currentProject } = useContext(Context); + const { currentProject, user } = useContext(Context); const markStepStarted = async ( { step, @@ -168,6 +168,18 @@ const CloudFormationForm: React.FC = ({ } markStepStarted({ step: "aws-create-integration-success", account_id: AWSAccountID }) proceed(`arn:aws:iam::${AWSAccountID}:role/porter-manager`); + + try { + window.dataLayer?.push({ + event: 'provision-attempt', + data: { + cloud: 'aws', + email: user?.email + } + }); + } catch (err) { + console.log(err); + } } const reportFailedCreateAWSIntegration = () => { @@ -431,4 +443,4 @@ const BackButton = styled.div` const AWSButtonContainer = styled.div` display: flex; align-items: center; - `; \ No newline at end of file + `; diff --git a/dashboard/src/components/GCPProvisionerSettings.tsx b/dashboard/src/components/GCPProvisionerSettings.tsx index 26838f7ab7b..8e39f5253b0 100644 --- a/dashboard/src/components/GCPProvisionerSettings.tsx +++ b/dashboard/src/components/GCPProvisionerSettings.tsx @@ -218,6 +218,20 @@ const GCPProvisionerSettings: React.FC = (props) => { setIsLoading(true); setIsClicked(true); + + + try { + window.dataLayer?.push({ + event: 'provision-attempt', + data: { + cloud: 'gcp', + email: user?.email + } + }); + } catch (err) { + console.log(err); + } + var data = new Contract({ cluster: new Cluster({ projectId: currentProject.id, diff --git a/dashboard/src/index.html b/dashboard/src/index.html index e7dae96d494..3a0c96553e7 100644 --- a/dashboard/src/index.html +++ b/dashboard/src/index.html @@ -2,6 +2,9 @@ +