From f845749a4d73f01e09f1473aaa9b01f0e7ec9f73 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Sat, 16 Mar 2024 16:08:59 -0700 Subject: [PATCH] Complete render integration auto-redeploy feature --- .../server/routes/v1/integration-router.ts | 1 + .../integration-sync-secret.ts | 16 ++ .../src/hooks/api/integrations/queries.tsx | 1 + .../src/pages/integrations/render/create.tsx | 209 ++++++++++++------ 4 files changed, 162 insertions(+), 65 deletions(-) diff --git a/backend/src/server/routes/v1/integration-router.ts b/backend/src/server/routes/v1/integration-router.ts index ed1914ccc0..670f838845 100644 --- a/backend/src/server/routes/v1/integration-router.ts +++ b/backend/src/server/routes/v1/integration-router.ts @@ -33,6 +33,7 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { secretPrefix: z.string().optional(), secretSuffix: z.string().optional(), initialSyncBehavior: z.string().optional(), + shouldAutoRedeploy: z.boolean().optional(), secretGCPLabel: z .object({ labelName: z.string(), diff --git a/backend/src/services/integration-auth/integration-sync-secret.ts b/backend/src/services/integration-auth/integration-sync-secret.ts index 43ae2dc821..3273b4b2d6 100644 --- a/backend/src/services/integration-auth/integration-sync-secret.ts +++ b/backend/src/services/integration-auth/integration-sync-secret.ts @@ -1307,6 +1307,22 @@ const syncSecretsRender = async ({ } } ); + + if (integration.metadata) { + const metadata = z.record(z.any()).parse(integration.metadata); + if (metadata.shouldAutoRedeploy === true) { + await request.post( + `${IntegrationUrls.RENDER_API_URL}/v1/services/${integration.appId}/deploys`, + {}, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + } + } }; /** diff --git a/frontend/src/hooks/api/integrations/queries.tsx b/frontend/src/hooks/api/integrations/queries.tsx index e50b914f16..74eb0b4656 100644 --- a/frontend/src/hooks/api/integrations/queries.tsx +++ b/frontend/src/hooks/api/integrations/queries.tsx @@ -62,6 +62,7 @@ export const useCreateIntegration = () => { secretPrefix?: string; secretSuffix?: string; initialSyncBehavior?: string; + shouldAutoRedeploy?: boolean; } }) => { const { data: { integration } } = await apiRequest.post("/api/v1/integration", { diff --git a/frontend/src/pages/integrations/render/create.tsx b/frontend/src/pages/integrations/render/create.tsx index a6342edd20..72bd304452 100644 --- a/frontend/src/pages/integrations/render/create.tsx +++ b/frontend/src/pages/integrations/render/create.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from "react"; +import { Controller, useForm } from "react-hook-form"; import Head from "next/head"; import Image from "next/image"; import Link from "next/link"; @@ -10,7 +11,9 @@ import { faCircleInfo } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { yupResolver } from "@hookform/resolvers/yup"; import queryString from "query-string"; +import * as yup from "yup"; import { useCreateIntegration } from "@app/hooks/api"; @@ -21,7 +24,8 @@ import { FormControl, Input, Select, - SelectItem + SelectItem, + Switch } from "../../../components/v2"; import { useGetIntegrationAuthApps, @@ -29,9 +33,28 @@ import { } from "../../../hooks/api/integrationAuth"; import { useGetWorkspaceById } from "../../../hooks/api/workspace"; +const schema = yup.object({ + selectedSourceEnvironment: yup.string().required("Source environment is required"), + secretPath: yup.string().required("Secret path is required"), + targetAppId: yup.string().required("Render service is required"), + shouldAutoRedeploy: yup.boolean() +}); + +type FormData = yup.InferType; + export default function RenderCreateIntegrationPage() { const router = useRouter(); const { mutateAsync } = useCreateIntegration(); + + const { control, handleSubmit, setValue, watch } = useForm({ + resolver: yupResolver(schema), + defaultValues: { + secretPath: "/", + shouldAutoRedeploy: false + } + }); + + const selectedSourceEnvironment = watch("selectedSourceEnvironment"); const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]); @@ -44,28 +67,29 @@ export default function RenderCreateIntegrationPage() { integrationAuthId: (integrationAuthId as string) ?? "" }); - const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(""); - const [targetApp, setTargetApp] = useState(""); - const [secretPath, setSecretPath] = useState("/"); const [isLoading, setIsLoading] = useState(false); useEffect(() => { if (workspace) { - setSelectedSourceEnvironment(workspace.environments[0].slug); + setValue("selectedSourceEnvironment", workspace.environments[0].slug); } }, [workspace]); useEffect(() => { if (integrationAuthApps) { if (integrationAuthApps.length > 0) { - setTargetApp(integrationAuthApps[0].name); + setValue("targetAppId", integrationAuthApps[0].appId as string); } else { - setTargetApp("none"); + setValue("targetAppId", "none"); } } }, [integrationAuthApps]); - - const handleButtonClick = async () => { + + const onFormSubmit = async ({ + secretPath, + targetAppId, + shouldAutoRedeploy + }: FormData) => { try { if (!integrationAuth?.id) return; @@ -74,12 +98,15 @@ export default function RenderCreateIntegrationPage() { await mutateAsync({ integrationAuthId: integrationAuth?.id, isActive: true, - app: targetApp, - appId: integrationAuthApps?.find( - (integrationAuthApp) => integrationAuthApp.name === targetApp - )?.appId, + app: integrationAuthApps?.find( + (integrationAuthApp) => integrationAuthApp.appId === targetAppId + )?.name, + appId: targetAppId, sourceEnvironment: selectedSourceEnvironment, - secretPath + secretPath, + metadata: { + shouldAutoRedeploy + } }); setIsLoading(false); @@ -88,14 +115,16 @@ export default function RenderCreateIntegrationPage() { } catch (err) { console.error(err); } - }; + } return integrationAuth && workspace && selectedSourceEnvironment && - integrationAuthApps && - targetApp ? ( -
+ integrationAuthApps ? ( +
Set Up Render Integration @@ -129,57 +158,107 @@ export default function RenderCreateIntegrationPage() {
- - - - - setSecretPath(evt.target.value)} - placeholder="Provide a path, default is /" - /> - - - + + )} + /> + ( + + + )} - - + /> + { + return ( + + + + ); + }} + /> +
+ ( + onChange(isChecked)} + isChecked={value} + > + Auto-redeploy service upon secret change + + )} + /> +
+