From d9ab38c59078eb082c784703c6328ff4dfc6b512 Mon Sep 17 00:00:00 2001 From: Chukwunonso Frank Date: Tue, 4 Jul 2023 22:52:23 +0100 Subject: [PATCH] chore: resolve merge conflicts --- .husky/pre-commit | 1 - backend/src/integrations/apps.ts | 47 +++++- backend/src/integrations/sync.ts | 149 ++++++++++++++---- backend/src/models/integration.ts | 9 +- backend/src/models/integrationAuth.ts | 8 +- backend/src/variables/integration.ts | 46 ++++-- .../integrations/northflank/authorize.tsx | 64 ++++++++ .../pages/integrations/northflank/create.tsx | 139 ++++++++++++++++ 8 files changed, 404 insertions(+), 59 deletions(-) create mode 100644 frontend/src/pages/integrations/northflank/authorize.tsx create mode 100644 frontend/src/pages/integrations/northflank/create.tsx diff --git a/.husky/pre-commit b/.husky/pre-commit index e235e5d7..4f18d252 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,3 @@ - #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" diff --git a/backend/src/integrations/apps.ts b/backend/src/integrations/apps.ts index bc8f01a8..3ac9e77a 100644 --- a/backend/src/integrations/apps.ts +++ b/backend/src/integrations/apps.ts @@ -9,17 +9,19 @@ import { INTEGRATION_CHECKLY_API_URL, INTEGRATION_CIRCLECI, INTEGRATION_CIRCLECI_API_URL, + INTEGRATION_CLOUDFLARE_PAGES, + INTEGRATION_CLOUDFLARE_PAGES_API_URL, INTEGRATION_FLYIO, INTEGRATION_FLYIO_API_URL, INTEGRATION_GITHUB, INTEGRATION_GITLAB, - INTEGRATION_CLOUDFLARE_PAGES, - INTEGRATION_CLOUDFLARE_PAGES_API_URL, INTEGRATION_GITLAB_API_URL, INTEGRATION_HEROKU, INTEGRATION_HEROKU_API_URL, INTEGRATION_NETLIFY, INTEGRATION_NETLIFY_API_URL, + INTEGRATION_NORTHFLANK, + INTEGRATION_NORTHFLANK_API_URL, INTEGRATION_RAILWAY, INTEGRATION_RAILWAY_API_URL, INTEGRATION_RENDER, @@ -29,7 +31,7 @@ import { INTEGRATION_TRAVISCI, INTEGRATION_TRAVISCI_API_URL, INTEGRATION_VERCEL, - INTEGRATION_VERCEL_API_URL, + INTEGRATION_VERCEL_API_URL } from "../variables"; interface App { @@ -135,7 +137,12 @@ const getApps = async ({ apps = await getAppsCloudflarePages({ accessToken, accountId: accessId - }) + }); + break; + case INTEGRATION_NORTHFLANK: + apps = await getAppsNorthflank({ + accessToken, + }); break; } @@ -678,5 +685,37 @@ const getAppsCloudflarePages = async ({ }); return apps; } + /* Return list of projects for Northflank integration + * @param {Object} obj + * @param {String} obj.accessToken - access token for Northflank API + * @returns {Object[]} apps - names of Northflank apps + * @returns {String} apps.name - name of Northflank app + */ +const getAppsNorthflank = async ({ accessToken }: { accessToken: string }) => { + const { + data: { + data: { + projects + } + } + } = await standardRequest.get( + `${INTEGRATION_NORTHFLANK_API_URL}/v1/projects`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json", + }, + } + ); + + const apps = projects.map((a: any) => { + return { + name: a.name, + appId: a.id, + }; + }); + + return apps; +}; export { getApps }; diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index 0cc3a943..a49b2aae 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -18,6 +18,8 @@ import { INTEGRATION_CHECKLY_API_URL, INTEGRATION_CIRCLECI, INTEGRATION_CIRCLECI_API_URL, + INTEGRATION_CLOUDFLARE_PAGES, + INTEGRATION_CLOUDFLARE_PAGES_API_URL, INTEGRATION_FLYIO, INTEGRATION_FLYIO_API_URL, INTEGRATION_GITHUB, @@ -28,14 +30,14 @@ import { INTEGRATION_HEROKU_API_URL, INTEGRATION_NETLIFY, INTEGRATION_NETLIFY_API_URL, + INTEGRATION_NORTHFLANK, + INTEGRATION_NORTHFLANK_API_URL, INTEGRATION_RAILWAY, INTEGRATION_RAILWAY_API_URL, INTEGRATION_RENDER, INTEGRATION_RENDER_API_URL, INTEGRATION_SUPABASE, INTEGRATION_SUPABASE_API_URL, - INTEGRATION_CLOUDFLARE_PAGES, - INTEGRATION_CLOUDFLARE_PAGES_API_URL, INTEGRATION_TRAVISCI, INTEGRATION_TRAVISCI_API_URL, INTEGRATION_VERCEL, @@ -168,34 +170,6 @@ const syncSecrets = async ({ accessToken, }); break; - case INTEGRATION_FLYIO: - await syncSecretsFlyio({ - integration, - secrets, - accessToken, - }); - break; - case INTEGRATION_CIRCLECI: - await syncSecretsCircleCI({ - integration, - secrets, - accessToken, - }); - break; - case INTEGRATION_TRAVISCI: - await syncSecretsTravisCI({ - integration, - secrets, - accessToken, - }); - break; - case INTEGRATION_SUPABASE: - await syncSecretsSupabase({ - integration, - secrets, - accessToken, - }); - break; case INTEGRATION_CHECKLY: await syncSecretsCheckly({ integration, @@ -220,6 +194,13 @@ const syncSecrets = async ({ accessToken }); break; + case INTEGRATION_NORTHFLANK: + await syncSecretsNorthflank({ + integration, + secrets, + accessToken + }); + break; } }; @@ -1874,7 +1855,7 @@ const syncSecretsCloudflarePages = async ({ } ) ) - .data.result['deployment_configs'][integration.targetEnvironment]['env_vars']; + .data.result["deployment_configs"][integration.targetEnvironment]["env_vars"]; // copy the secrets object, so we can set deleted keys to null const secretsObj: any = {...secrets}; @@ -1912,5 +1893,111 @@ const syncSecretsCloudflarePages = async ({ } ); } + /* Sync/push [secrets] to Northflank + * @param {Object} obj + * @param {IIntegration} obj.integration - integration 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 - access token for Northflank integration + */ +const syncSecretsNorthflank = async ({ + integration, + secrets, + accessToken +}: { + integration: IIntegration; + secrets: any; + accessToken: string; +}) => { + +// secrets: { +// secretGroupID: 'some_id', +// secretGroupName: 'some_name', +// data: {} +// } + + const { + data: { + secrets: getSecretsRes + } + } = await standardRequest.get( + `${INTEGRATION_NORTHFLANK_API_URL}/v1/projects/${integration.appId}/secrets`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + + const secretGroups = getSecretsRes.map((group: any) => { + return { + id: group.id, + name: group.name + }; + }) + + for await (const group of secretGroups) { + if (group.id === secrets.secretGroupID) { + // add secret to existing group + let { + data: { + secrets: { + variables + } + } + } = await standardRequest.get( + `${INTEGRATION_NORTHFLANK_API_URL}/v1/projects/${integration.appId}/secrets/${secrets.secretGroupID}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + + variables = { ...secrets.data } + + const modifiedFormatForSecretInjection = { + secrets: { + variables + } + } + + await standardRequest.post( + `${INTEGRATION_NORTHFLANK_API_URL}/v1/projects/${integration.appId}/secrets/${secrets.secretGroupID}`, + modifiedFormatForSecretInjection, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + } else { + // create new secret group + const modifiedFormatForSecretInjection = { + name: secrets.secretGroupName, + secretType: "environment", + priority: 10, + secrets: { + variables: secrets.data + } + }; + + await standardRequest.post( + `${INTEGRATION_NORTHFLANK_API_URL}/v1/projects/${integration.appId}/secrets`, + modifiedFormatForSecretInjection, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + } + } + + // TODO:: figure out delete business logic for secret groups +}; export { syncSecrets }; diff --git a/backend/src/models/integration.ts b/backend/src/models/integration.ts index d94cc9fa..f952f83b 100644 --- a/backend/src/models/integration.ts +++ b/backend/src/models/integration.ts @@ -5,18 +5,19 @@ import { INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_CHECKLY, INTEGRATION_CIRCLECI, + INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_FLYIO, INTEGRATION_GITHUB, INTEGRATION_GITLAB, INTEGRATION_HASHICORP_VAULT, INTEGRATION_HEROKU, INTEGRATION_NETLIFY, + INTEGRATION_NORTHFLANK, INTEGRATION_RAILWAY, INTEGRATION_RENDER, INTEGRATION_SUPABASE, - INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_TRAVISCI, - INTEGRATION_VERCEL, + INTEGRATION_VERCEL } from "../variables"; export interface IIntegration { @@ -52,7 +53,8 @@ export interface IIntegration { | "supabase" | "checkly" | "hashicorp-vault" - | "cloudflare-pages"; + | "cloudflare-pages" + | "northflank"; integrationAuth: Types.ObjectId; } @@ -141,6 +143,7 @@ const integrationSchema = new Schema( INTEGRATION_CHECKLY, INTEGRATION_HASHICORP_VAULT, INTEGRATION_CLOUDFLARE_PAGES, + INTEGRATION_NORTHFLANK ], required: true, }, diff --git a/backend/src/models/integrationAuth.ts b/backend/src/models/integrationAuth.ts index 2d56c206..79013415 100644 --- a/backend/src/models/integrationAuth.ts +++ b/backend/src/models/integrationAuth.ts @@ -7,24 +7,25 @@ import { INTEGRATION_AWS_SECRET_MANAGER, INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_CIRCLECI, + INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_FLYIO, INTEGRATION_GITHUB, INTEGRATION_GITLAB, INTEGRATION_HASHICORP_VAULT, INTEGRATION_HEROKU, INTEGRATION_NETLIFY, + INTEGRATION_NORTHFLANK, INTEGRATION_RAILWAY, INTEGRATION_RENDER, INTEGRATION_SUPABASE, - INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_TRAVISCI, - INTEGRATION_VERCEL, + INTEGRATION_VERCEL } from "../variables"; export interface IIntegrationAuth extends Document { _id: Types.ObjectId; workspace: Types.ObjectId; - integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'gitlab' | 'render' | 'railway' | 'flyio' | 'azure-key-vault' | 'circleci' | 'travisci' | 'supabase' | 'aws-parameter-store' | 'aws-secret-manager' | 'checkly' | 'cloudflare-pages'; + integration: "heroku" | "vercel" | "netlify" | "github" | "gitlab" | "render" | "railway" | "flyio" | "azure-key-vault" | "circleci" | "travisci" | "supabase" | "aws-parameter-store" | "aws-secret-manager" | "checkly" | "cloudflare-pages" | "northflank"; teamId: string; accountId: string; url: string; @@ -69,6 +70,7 @@ const integrationAuthSchema = new Schema( INTEGRATION_SUPABASE, INTEGRATION_HASHICORP_VAULT, INTEGRATION_CLOUDFLARE_PAGES, + INTEGRATION_NORTHFLANK ], required: true, }, diff --git a/backend/src/variables/integration.ts b/backend/src/variables/integration.ts index c87d2eb9..4ceaa9ed 100644 --- a/backend/src/variables/integration.ts +++ b/backend/src/variables/integration.ts @@ -21,10 +21,11 @@ export const INTEGRATION_RAILWAY = "railway"; export const INTEGRATION_FLYIO = "flyio"; export const INTEGRATION_CIRCLECI = "circleci"; export const INTEGRATION_TRAVISCI = "travisci"; -export const INTEGRATION_SUPABASE = 'supabase'; -export const INTEGRATION_CHECKLY = 'checkly'; -export const INTEGRATION_HASHICORP_VAULT = 'hashicorp-vault'; -export const INTEGRATION_CLOUDFLARE_PAGES = 'cloudflare-pages'; +export const INTEGRATION_SUPABASE = "supabase"; +export const INTEGRATION_CHECKLY = "checkly"; +export const INTEGRATION_HASHICORP_VAULT = "hashicorp-vault"; +export const INTEGRATION_CLOUDFLARE_PAGES = "cloudflare-pages"; +export const INTEGRATION_NORTHFLANK = "northflank"; export const INTEGRATION_SET = new Set([ INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_HEROKU, @@ -39,7 +40,8 @@ export const INTEGRATION_SET = new Set([ INTEGRATION_SUPABASE, INTEGRATION_CHECKLY, INTEGRATION_HASHICORP_VAULT, - INTEGRATION_CLOUDFLARE_PAGES + INTEGRATION_CLOUDFLARE_PAGES, + INTEGRATION_NORTHFLANK ]); // integration types @@ -65,9 +67,10 @@ export const INTEGRATION_RAILWAY_API_URL = "https://backboard.railway.app/graphq export const INTEGRATION_FLYIO_API_URL = "https://api.fly.io/graphql"; export const INTEGRATION_CIRCLECI_API_URL = "https://circleci.com/api"; export const INTEGRATION_TRAVISCI_API_URL = "https://api.travis-ci.com"; -export const INTEGRATION_SUPABASE_API_URL = 'https://api.supabase.com'; -export const INTEGRATION_CHECKLY_API_URL = 'https://api.checklyhq.com'; -export const INTEGRATION_CLOUDFLARE_PAGES_API_URL = 'https://api.cloudflare.com'; +export const INTEGRATION_SUPABASE_API_URL = "https://api.supabase.com"; +export const INTEGRATION_CHECKLY_API_URL = "https://api.checklyhq.com"; +export const INTEGRATION_CLOUDFLARE_PAGES_API_URL = "https://api.cloudflare.com"; +export const INTEGRATION_NORTHFLANK_API_URL = "https://api.northflank.com"; export const getIntegrationOptions = async () => { const INTEGRATION_OPTIONS = [ @@ -221,18 +224,27 @@ export const getIntegrationOptions = async () => { slug: "gcp", image: "Google Cloud Platform.png", isAvailable: false, - type: '', - clientId: '', - docsLink: '' + type: "", + clientId: "", + docsLink: "" + }, + { + name: "Cloudflare Pages", + slug: "cloudflare-pages", + image: "Cloudflare.png", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "" }, { - name: 'Cloudflare Pages', - slug: 'cloudflare-pages', - image: 'Cloudflare.png', + name: "Northflank", + slug: "northflank", + image: "Northflank.png", isAvailable: true, - type: 'pat', - clientId: '', - docsLink: '' + type: "pat", + clientId: "", + docsLink: "" } ] diff --git a/frontend/src/pages/integrations/northflank/authorize.tsx b/frontend/src/pages/integrations/northflank/authorize.tsx new file mode 100644 index 00000000..8e2baa3c --- /dev/null +++ b/frontend/src/pages/integrations/northflank/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 NorthflankCreateIntegrationPage() { + 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: "northflank", + accessToken: apiKey, + accessId: null, + url: null, + namespace: null + }); + + setIsLoading(false); + + router.push(`/integrations/northflank/create?integrationAuthId=${integrationAuth._id}`); + } catch (err) { + console.error(err); + } + }; + + return ( +
+ + Northflank Integration + + setApiKey(e.target.value)} /> + + + +
+ ); +} + +NorthflankCreateIntegrationPage.requireAuth = true; diff --git a/frontend/src/pages/integrations/northflank/create.tsx b/frontend/src/pages/integrations/northflank/create.tsx new file mode 100644 index 00000000..12e4b1e9 --- /dev/null +++ b/frontend/src/pages/integrations/northflank/create.tsx @@ -0,0 +1,139 @@ +import { useEffect, useState } from "react"; +import { useRouter } from "next/router"; +import queryString from "query-string"; + +import { Button, Card, CardTitle, FormControl, 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 NorthflankCreateIntegrationPage() { + 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 [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 + }); + + setIsLoading(false); + + router.push(`/integrations/${localStorage.getItem("projectData.id")}`); + } catch (err) { + console.error(err); + } + }; + + return integrationAuth && + workspace && + selectedSourceEnvironment && + integrationAuthApps && + targetApp ? ( +
+ + Northflank Integration + + + + + + + + +
+ ) : ( +
+ ); +} + +NorthflankCreateIntegrationPage.requireAuth = true;