From b5fb2ef354e3ee5bc2c5f267c1a759c69ee3529d Mon Sep 17 00:00:00 2001 From: Afrie Irham Date: Sun, 23 Jul 2023 22:01:48 +0800 Subject: [PATCH] feat: DO app platform integration --- backend/src/integrations/apps.ts | 47 ++++++ backend/src/integrations/sync.ts | 35 ++++ backend/src/models/integration.ts | 3 + backend/src/models/integrationAuth.ts | 3 + backend/src/variables/integration.ts | 14 +- frontend/public/data/frequentConstants.ts | 1 + .../digital-ocean-app-platform/authorize.tsx | 64 ++++++++ .../digital-ocean-app-platform/create.tsx | 155 ++++++++++++++++++ .../IntegrationPage.utils.tsx | 3 + 9 files changed, 324 insertions(+), 1 deletion(-) create mode 100644 frontend/src/pages/integrations/digital-ocean-app-platform/authorize.tsx create mode 100644 frontend/src/pages/integrations/digital-ocean-app-platform/create.tsx diff --git a/backend/src/integrations/apps.ts b/backend/src/integrations/apps.ts index dea45ac99..ec8676d1d 100644 --- a/backend/src/integrations/apps.ts +++ b/backend/src/integrations/apps.ts @@ -15,6 +15,8 @@ import { INTEGRATION_CLOUDFLARE_PAGES_API_URL, INTEGRATION_CODEFRESH, INTEGRATION_CODEFRESH_API_URL, + INTEGRATION_DIGITAL_OCEAN_API_URL, + INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, INTEGRATION_FLYIO, INTEGRATION_FLYIO_API_URL, INTEGRATION_GITHUB, @@ -162,6 +164,9 @@ const getApps = async ({ accessToken, }); break; + case INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM: + apps = await getAppsDigitalOceanAppPlatform({ accessToken }); + break; } return apps; @@ -842,4 +847,46 @@ const getAppsCodefresh = async ({ return apps; }; + +/** + * Return list of projects for Digital Ocean App Platform integration + */ + +const getAppsDigitalOceanAppPlatform = async ({ accessToken }: { accessToken: string }) => { + interface DigitalOceanApp { + id: string; + owner_uuid: string; + spec: Spec; + } + + interface Spec { + name: string; + region: string; + envs: Env[]; + } + + interface Env { + key: string; + value: string; + scope: string; + } + + const res = ( + await standardRequest.get(`${INTEGRATION_DIGITAL_OCEAN_API_URL}/v2/apps`, { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + }) + ).data; + + const apps = res.apps.map((a: DigitalOceanApp) => ({ + name: a.spec.name, + appId: a.id + })); + + return apps; +}; + + export { getApps }; diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index 2c2ed8d1f..5da5531ff 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -24,6 +24,8 @@ import { INTEGRATION_CLOUDFLARE_PAGES_API_URL, INTEGRATION_CODEFRESH, INTEGRATION_CODEFRESH_API_URL, + INTEGRATION_DIGITAL_OCEAN_API_URL, + INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, INTEGRATION_FLYIO, INTEGRATION_FLYIO_API_URL, INTEGRATION_GITHUB, @@ -220,6 +222,13 @@ const syncSecrets = async ({ accessToken, }); break; + case INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM: + await syncSecretsDigitalOceanAppPlatform({ + integration, + secrets, + accessToken, + }); + break; } }; @@ -2101,4 +2110,30 @@ const syncSecretsCodefresh = async ({ ); }; +const syncSecretsDigitalOceanAppPlatform = async ({ + integration, + secrets, + accessToken +}: { + integration: IIntegration; + secrets: any; + accessToken: string; +}) => { + await standardRequest.put( + `${INTEGRATION_DIGITAL_OCEAN_API_URL}/v2/apps/${integration.appId}`, + { + spec: { + name: integration.app, + envs: Object.entries(secrets).map(([key, value]) => ({ key, value })) + } + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } + } + ); +}; + export { syncSecrets }; diff --git a/backend/src/models/integration.ts b/backend/src/models/integration.ts index 05e089f37..bcaf4cd27 100644 --- a/backend/src/models/integration.ts +++ b/backend/src/models/integration.ts @@ -8,6 +8,7 @@ import { INTEGRATION_CIRCLECI, INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_CODEFRESH, + INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, INTEGRATION_FLYIO, INTEGRATION_GITHUB, INTEGRATION_GITLAB, @@ -59,6 +60,7 @@ export interface IIntegration { | "cloudflare-pages" | "bitbucket" | "codefresh" + | "digital-ocean-app-platform" integrationAuth: Types.ObjectId; } @@ -149,6 +151,7 @@ const integrationSchema = new Schema( INTEGRATION_HASHICORP_VAULT, INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_BITBUCKET, + INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, INTEGRATION_CODEFRESH ], required: true, diff --git a/backend/src/models/integrationAuth.ts b/backend/src/models/integrationAuth.ts index 409249bf3..31b9d39c8 100644 --- a/backend/src/models/integrationAuth.ts +++ b/backend/src/models/integrationAuth.ts @@ -10,6 +10,7 @@ import { INTEGRATION_CIRCLECI, INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_CODEFRESH, + INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, INTEGRATION_FLYIO, INTEGRATION_GITHUB, INTEGRATION_GITLAB, @@ -46,6 +47,7 @@ export interface IIntegrationAuth extends Document { | "checkly" | "cloudflare-pages" | "codefresh" + | "digital-ocean-app-platform" | "bitbucket"; teamId: string; accountId: string; @@ -93,6 +95,7 @@ const integrationAuthSchema = new Schema( INTEGRATION_HASHICORP_VAULT, INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_BITBUCKET, + INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, INTEGRATION_CODEFRESH ], required: true, diff --git a/backend/src/variables/integration.ts b/backend/src/variables/integration.ts index e99b5a3bf..1f5dfca1f 100644 --- a/backend/src/variables/integration.ts +++ b/backend/src/variables/integration.ts @@ -29,6 +29,7 @@ export const INTEGRATION_HASHICORP_VAULT = "hashicorp-vault"; export const INTEGRATION_CLOUDFLARE_PAGES = "cloudflare-pages"; export const INTEGRATION_BITBUCKET = "bitbucket"; export const INTEGRATION_CODEFRESH = "codefresh"; +export const INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM = "digital-ocean-app-platform"; export const INTEGRATION_SET = new Set([ INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_HEROKU, @@ -46,6 +47,7 @@ export const INTEGRATION_SET = new Set([ INTEGRATION_HASHICORP_VAULT, INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_BITBUCKET, + INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, INTEGRATION_CODEFRESH ]); @@ -79,6 +81,7 @@ export const INTEGRATION_CHECKLY_API_URL = "https://api.checklyhq.com"; export const INTEGRATION_CLOUDFLARE_PAGES_API_URL = "https://api.cloudflare.com"; export const INTEGRATION_BITBUCKET_API_URL = "https://api.bitbucket.org"; export const INTEGRATION_CODEFRESH_API_URL = "https://g.codefresh.io/api"; +export const INTEGRATION_DIGITAL_OCEAN_API_URL = "https://api.digitalocean.com"; export const getIntegrationOptions = async () => { const INTEGRATION_OPTIONS = [ @@ -271,7 +274,16 @@ export const getIntegrationOptions = async () => { type: "pat", clientId: "", docsLink: "", - } + }, + { + name: "Digital Ocean App Platform", + slug: "digital-ocean-app-platform", + image: "Digital Ocean.png", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "", + }, ] return INTEGRATION_OPTIONS; diff --git a/frontend/public/data/frequentConstants.ts b/frontend/public/data/frequentConstants.ts index d4c0aa729..f07415e41 100644 --- a/frontend/public/data/frequentConstants.ts +++ b/frontend/public/data/frequentConstants.ts @@ -22,6 +22,7 @@ const integrationSlugNameMapping: Mapping = { "hashicorp-vault": "Vault", "cloudflare-pages": "Cloudflare Pages", "codefresh": "Codefresh", + "digital-ocean-app-platform": "Digital Ocean App Platform", bitbucket: "BitBucket" }; diff --git a/frontend/src/pages/integrations/digital-ocean-app-platform/authorize.tsx b/frontend/src/pages/integrations/digital-ocean-app-platform/authorize.tsx new file mode 100644 index 000000000..9c3912b2e --- /dev/null +++ b/frontend/src/pages/integrations/digital-ocean-app-platform/authorize.tsx @@ -0,0 +1,64 @@ +import { useState } from "react"; +import { useRouter } from "next/router"; + +import { Button, Card, CardTitle, FormControl, Input } from "../../../components/v2"; +import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken"; + +export default function DigitalOceanAppPlatformCreateIntegrationPage() { + const router = useRouter(); + const [apiKey, setApiKey] = useState(""); + const [apiKeyErrorText, setApiKeyErrorText] = useState(""); + const [isLoading, setIsLoading] = useState(false); + + const handleButtonClick = async () => { + try { + setApiKeyErrorText(""); + if (apiKey.length === 0) { + setApiKeyErrorText("API Key cannot be blank"); + return; + } + + setIsLoading(true); + + const integrationAuth = await saveIntegrationAccessToken({ + workspaceId: localStorage.getItem("projectData.id"), + integration: "digital-ocean-app-platform", + accessId: null, + accessToken: apiKey, + url: null, + namespace: null + }); + + setIsLoading(false); + + router.push(`/integrations/digital-ocean-app-platform/create?integrationAuthId=${integrationAuth._id}`); + } catch (err) { + console.error(err); + } + }; + + return ( +
+ + Digital Ocean App Platform Integration + + setApiKey(e.target.value)} /> + + + +
+ ); +} + +DigitalOceanAppPlatformCreateIntegrationPage.requireAuth = true; diff --git a/frontend/src/pages/integrations/digital-ocean-app-platform/create.tsx b/frontend/src/pages/integrations/digital-ocean-app-platform/create.tsx new file mode 100644 index 000000000..925729c94 --- /dev/null +++ b/frontend/src/pages/integrations/digital-ocean-app-platform/create.tsx @@ -0,0 +1,155 @@ +import { useEffect, useState } from "react"; +import { useRouter } from "next/router"; +import queryString from "query-string"; + +import { + Button, + Card, + CardTitle, + FormControl, + Input, + Select, + SelectItem +} from "../../../components/v2"; +import { + useGetIntegrationAuthApps, + useGetIntegrationAuthById +} from "../../../hooks/api/integrationAuth"; +import { useGetWorkspaceById } from "../../../hooks/api/workspace"; +import createIntegration from "../../api/integrations/createIntegration"; + +export default function DigitalOceanAppPlatformCreateIntegrationPage() { + const router = useRouter(); + + const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]); + + const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? ""); + const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? ""); + const { data: integrationAuthApps } = useGetIntegrationAuthApps({ + 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); + } + }, [workspace]); + + useEffect(() => { + if (integrationAuthApps) { + if (integrationAuthApps.length > 0) { + setTargetApp(integrationAuthApps[0].name); + } else { + setTargetApp("none"); + } + } + }, [integrationAuthApps]); + + const handleButtonClick = async () => { + try { + if (!integrationAuth?._id) return; + + setIsLoading(true); + + await createIntegration({ + integrationAuthId: integrationAuth?._id, + isActive: true, + app: targetApp, + appId: + integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp) + ?.appId ?? null, + sourceEnvironment: selectedSourceEnvironment, + targetEnvironment: null, + targetEnvironmentId: null, + targetService: null, + targetServiceId: null, + owner: null, + path: null, + region: null, + secretPath + }); + + setIsLoading(false); + + router.push(`/integrations/${localStorage.getItem("projectData.id")}`); + } catch (err) { + console.error(err); + } + }; + + return integrationAuth && + workspace && + selectedSourceEnvironment && + integrationAuthApps && + targetApp ? ( +
+ + Digital Ocean App Platform Integration + + + + + setSecretPath(evt.target.value)} + placeholder="Provide a path, default is /" + /> + + + + + + +
+ ) : ( +
+ ); +} + +DigitalOceanAppPlatformCreateIntegrationPage.requireAuth = true; diff --git a/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx b/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx index a5be32502..20c65a6ad 100644 --- a/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx +++ b/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx @@ -98,6 +98,9 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) => case "codefresh": link = `${window.location.origin}/integrations/codefresh/authorize`; break; + case "digital-ocean-app-platform": + link = `${window.location.origin}/integrations/digital-ocean-app-platform/authorize`; + break; default: break; }