diff --git a/backend/src/integrations/apps.ts b/backend/src/integrations/apps.ts index 51cf43c45..69810d025 100644 --- a/backend/src/integrations/apps.ts +++ b/backend/src/integrations/apps.ts @@ -14,6 +14,8 @@ import { INTEGRATION_CLOUD_66_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, @@ -164,6 +166,11 @@ const getApps = async ({ accessToken, }); break; + case INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM: + apps = await getAppsDigitalOceanAppPlatform({ + accessToken + }); + break; case INTEGRATION_CLOUD_66: apps = await getAppsCloud66({ accessToken, @@ -849,6 +856,48 @@ const getAppsCodefresh = async ({ }; +/** + * Return list of applications for DigitalOcean App Platform integration + * @param {Object} obj + * @param {String} obj.accessToken - personal access token for DigitalOcean + * @returns {Object[]} apps - names of DigitalOcean apps + * @returns {String} apps.name - name of DigitalOcean app + * @returns {String} apps.appId - id of DigitalOcean app + */ +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; + + return (res.apps ?? []).map((a: DigitalOceanApp) => ({ + name: a.spec.name, + appId: a.id + })); +} + /** * Return list of applications for Cloud66 integration * @param {Object} obj diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index aef9c52fe..9afb0e4c5 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -22,6 +22,8 @@ import { INTEGRATION_CLOUD_66_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, @@ -222,6 +224,13 @@ const syncSecrets = async ({ accessToken, }); break; + case INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM: + await syncSecretsDigitalOceanAppPlatform({ + integration, + secrets, + accessToken, + }); + break; case INTEGRATION_CLOUD_66: await syncSecretsCloud66({ integration, @@ -2111,6 +2120,40 @@ const syncSecretsCodefresh = async ({ ); }; +/** + * Sync/push [secrets] to DigitalOcean App Platform application with name [integration.app] + * @param {Object} obj + * @param {IIntegration} obj.integration - integration details + * @param {IIntegrationAuth} obj.integrationAuth - integration auth details + * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) + * @param {String} obj.accessToken - personal access token for DigitalOcean + */ +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" + } + } + ); +} + /** * Sync/push [secrets] to Cloud66 application with name [integration.app] * @param {Object} obj @@ -2128,6 +2171,7 @@ const syncSecretsCloud66 = async ({ secrets: any; accessToken: string; }) => { + interface Cloud66Secret { id: number; key: string; diff --git a/backend/src/models/integration.ts b/backend/src/models/integration.ts index f3234b616..e79e85ce9 100644 --- a/backend/src/models/integration.ts +++ b/backend/src/models/integration.ts @@ -8,6 +8,7 @@ import { INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_CLOUD_66, INTEGRATION_CODEFRESH, + INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, INTEGRATION_FLYIO, INTEGRATION_GITHUB, INTEGRATION_GITLAB, @@ -60,6 +61,7 @@ export interface IIntegration { | "cloudflare-pages" | "bitbucket" | "codefresh" + | "digital-ocean-app-platform" | "cloud-66" integrationAuth: Types.ObjectId; } @@ -151,6 +153,7 @@ const integrationSchema = new Schema( INTEGRATION_HASHICORP_VAULT, INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_BITBUCKET, + INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, INTEGRATION_CODEFRESH, INTEGRATION_CLOUD_66, ], diff --git a/backend/src/models/integrationAuth.ts b/backend/src/models/integrationAuth.ts index b1f4e4c7e..8d1ea1e56 100644 --- a/backend/src/models/integrationAuth.ts +++ b/backend/src/models/integrationAuth.ts @@ -10,6 +10,7 @@ import { INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_CLOUD_66, INTEGRATION_CODEFRESH, + INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, INTEGRATION_FLYIO, INTEGRATION_GITHUB, INTEGRATION_GITLAB, @@ -47,6 +48,7 @@ export interface IIntegrationAuth extends Document { | "checkly" | "cloudflare-pages" | "codefresh" + | "digital-ocean-app-platform" | "bitbucket" | "cloud-66"; teamId: string; @@ -95,6 +97,7 @@ const integrationAuthSchema = new Schema( INTEGRATION_HASHICORP_VAULT, INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_BITBUCKET, + INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, INTEGRATION_CODEFRESH, INTEGRATION_CLOUD_66, ], diff --git a/backend/src/variables/integration.ts b/backend/src/variables/integration.ts index c005ac715..95c5b7116 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_CLOUD_66 = "cloud-66"; export const INTEGRATION_SET = new Set([ INTEGRATION_AZURE_KEY_VAULT, @@ -47,6 +48,7 @@ export const INTEGRATION_SET = new Set([ INTEGRATION_HASHICORP_VAULT, INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_BITBUCKET, + INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, INTEGRATION_CODEFRESH, INTEGRATION_CLOUD_66 ]); @@ -81,6 +83,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 INTEGRATION_CLOUD_66_API_URL = "https://app.cloud66.com/api"; export const getIntegrationOptions = async () => { @@ -275,6 +278,15 @@ export const getIntegrationOptions = async () => { clientId: "", docsLink: "", }, + { + name: "Digital Ocean App Platform", + slug: "digital-ocean-app-platform", + image: "Digital Ocean.png", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "", + }, { name: "Cloud 66", slug: "cloud-66", diff --git a/docs/images/integrations-do-dashboard.png b/docs/images/integrations-do-dashboard.png new file mode 100644 index 000000000..af9a06d07 Binary files /dev/null and b/docs/images/integrations-do-dashboard.png differ diff --git a/docs/images/integrations-do-enter-token.png b/docs/images/integrations-do-enter-token.png new file mode 100644 index 000000000..48a436278 Binary files /dev/null and b/docs/images/integrations-do-enter-token.png differ diff --git a/docs/images/integrations-do-select-projects.png b/docs/images/integrations-do-select-projects.png new file mode 100644 index 000000000..cb1878f5f Binary files /dev/null and b/docs/images/integrations-do-select-projects.png differ diff --git a/docs/images/integrations-do-success.png b/docs/images/integrations-do-success.png new file mode 100644 index 000000000..3866c4a7d Binary files /dev/null and b/docs/images/integrations-do-success.png differ diff --git a/docs/images/integrations-do-token-modal.png b/docs/images/integrations-do-token-modal.png new file mode 100644 index 000000000..47e8a27fe Binary files /dev/null and b/docs/images/integrations-do-token-modal.png differ diff --git a/docs/images/integrations.png b/docs/images/integrations.png index 89c4c6d66..af2a45125 100644 Binary files a/docs/images/integrations.png and b/docs/images/integrations.png differ diff --git a/docs/integrations/cloud/digital-ocean-app-platform.mdx b/docs/integrations/cloud/digital-ocean-app-platform.mdx new file mode 100644 index 000000000..10dd7751d --- /dev/null +++ b/docs/integrations/cloud/digital-ocean-app-platform.mdx @@ -0,0 +1,39 @@ +--- +title: "Digital Ocean App Platform" +description: "How to sync secrets from Infisical to Digital Ocean App Platform" +--- + +Prerequisites: + +- Set up and add envars to [Infisical Cloud](https://app.infisical.com) + +## Get your Digital Ocean Personal Access Tokens + +On Digital Ocean dashboard, navigate to **API > Tokens** and click on "Generate New Token" +![integrations digital ocean dashboard](../../images/integrations-do-dashboard.png) + +Name it **infisical**, choose **No expiry**, and make sure to check **Write (optional)**. Then click on "Generate Token" and copy your API token. +![integrations digital ocean token modal](../../images/integrations-do-token-modal.png) + +## Navigate to your project's integrations tab + +Click on the **Digital Ocean App Platform** tile and enter your API token to grant Infisical access to your Digital Ocean account. +![integrations](../../images/integrations.png) + + + If this is your project's first cloud integration, then you'll have to grant + Infisical access to your project's environment variables. Although this step + breaks E2EE, it's necessary for Infisical to sync the environment variables to + the cloud platform. + + +Then enter your Digital Ocean Personal Access Token here. Then click "Connect to Digital Ocean App Platform". +![integrations infisical dashboard digital ocean integration](../../images/integrations-do-enter-token.png) + +## Start integration + +Select which Infisical environment secrets you want to sync to which Digital Ocean App and click "Create Integration". +![integrations digital ocean select projects](../../images/integrations-do-select-projects.png) + +Done! +![integrations digital ocean integration success](../../images/integrations-do-success.png) diff --git a/docs/mint.json b/docs/mint.json index ba4324027..48755ea92 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -208,6 +208,12 @@ "integrations/cloud/aws-secret-manager" ] }, + { + "group": "Digital Ocean", + "pages": [ + "integrations/cloud/digital-ocean-app-platform" + ] + }, "integrations/cloud/heroku", "integrations/cloud/vercel", "integrations/cloud/netlify", diff --git a/frontend/public/data/frequentConstants.ts b/frontend/public/data/frequentConstants.ts index 996860826..61490758e 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", "cloud-66": "Cloud 66" }; 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 9a3316822..7ec4a15a8 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; case "cloud-66": link = `${window.location.origin}/integrations/cloud-66/authorize`; break; diff --git a/frontend/src/views/IntegrationsPage/components/CloudIntegrationSection/CloudIntegrationSection.tsx b/frontend/src/views/IntegrationsPage/components/CloudIntegrationSection/CloudIntegrationSection.tsx index 4ce1bd083..e8e761a0f 100644 --- a/frontend/src/views/IntegrationsPage/components/CloudIntegrationSection/CloudIntegrationSection.tsx +++ b/frontend/src/views/IntegrationsPage/components/CloudIntegrationSection/CloudIntegrationSection.tsx @@ -67,18 +67,9 @@ export const CloudIntegrationSection = ({ width={70} alt="integration logo" /> - {cloudIntegration.name.split(" ").length > 2 ? ( -
-
{cloudIntegration.name.split(" ")[0]}
-
- {cloudIntegration.name.split(" ")[1]} {cloudIntegration.name.split(" ")[2]} -
-
- ) : ( -
- {cloudIntegration.name} -
- )} +
+ {cloudIntegration.name} +
{cloudIntegration.isAvailable && Boolean(integrationAuths?.[cloudIntegration.slug]) && (