From a0a7ff8715769688ffa046969e24d99a347bc62b Mon Sep 17 00:00:00 2001 From: chisom okoye Date: Wed, 19 Jul 2023 16:22:25 +0100 Subject: [PATCH] Codefresh integration Worked on codefresh integration syncing secrets to infiscial --- backend/src/integrations/apps.ts | 89 ++- backend/src/integrations/sync.ts | 533 +++++++++++------- backend/src/models/integration.ts | 5 +- backend/src/models/integrationAuth.ts | 4 +- backend/src/variables/integration.ts | 18 +- .../public/images/integrations/codefresh.png | Bin 0 -> 34956 bytes .../src/hooks/api/integrationAuth/index.tsx | 2 +- .../src/hooks/api/integrationAuth/queries.tsx | 4 + .../integrations/codefresh/authorize.tsx | 64 +++ .../pages/integrations/codefresh/create.tsx | 155 +++++ .../IntegrationPage.utils.tsx | 4 + .../IntegrationsPage/IntegrationsPage.tsx | 1 + .../IntegrationsSection.tsx | 1 + 13 files changed, 641 insertions(+), 239 deletions(-) create mode 100644 frontend/public/images/integrations/codefresh.png create mode 100644 frontend/src/pages/integrations/codefresh/authorize.tsx create mode 100644 frontend/src/pages/integrations/codefresh/create.tsx diff --git a/backend/src/integrations/apps.ts b/backend/src/integrations/apps.ts index bc8f01a80..2b5940998 100644 --- a/backend/src/integrations/apps.ts +++ b/backend/src/integrations/apps.ts @@ -30,6 +30,9 @@ import { INTEGRATION_TRAVISCI_API_URL, INTEGRATION_VERCEL, INTEGRATION_VERCEL_API_URL, + INTEGRATION_CODEFRESH, + INTEGRATION_CODEFRESH_API_URL + } from "../variables"; interface App { @@ -137,6 +140,11 @@ const getApps = async ({ accountId: accessId }) break; + case INTEGRATION_CODEFRESH: + apps = await getAppsCodefresh({ + accessToken, + }); + break; } return apps; @@ -188,10 +196,10 @@ const getAppsVercel = async ({ }, ...(integrationAuth?.teamId ? { - params: { - teamId: integrationAuth.teamId, - }, - } + params: { + teamId: integrationAuth.teamId, + }, + } : {}), }) ).data; @@ -653,30 +661,61 @@ const getAppsCheckly = async ({ accessToken }: { accessToken: string }) => { * @returns {Object[]} apps - Cloudflare Pages projects * @returns {String} apps.name - name of Cloudflare Pages project */ -const getAppsCloudflarePages = async ({ - accessToken, - accountId +const getAppsCloudflarePages = async ({ + accessToken, + accountId }: { - accessToken: string; - accountId?: string; + accessToken: string; + accountId?: string; }) => { - const { data } = await standardRequest.get( - `${INTEGRATION_CLOUDFLARE_PAGES_API_URL}/client/v4/accounts/${accountId}/pages/projects`, - { - headers: { - Authorization: `Bearer ${accessToken}`, - "Accept": "application/json", - }, - } - ); + const { data } = await standardRequest.get( + `${INTEGRATION_CLOUDFLARE_PAGES_API_URL}/client/v4/accounts/${accountId}/pages/projects`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept": "application/json", + }, + } + ); - const apps = data.result.map((a: any) => { - return { - name: a.name, - appId: a.id, - }; - }); - return apps; + const apps = data.result.map((a: any) => { + return { + name: a.name, + appId: a.id, + }; + }); + return apps; } + +/** + * Return list of projects for Supabase integration + * @param {Object} obj + * @param {String} obj.accessToken - access token for Supabase API + * @returns {Object[]} apps - names of Supabase apps + * @returns {String} apps.name - name of Supabase app + */ + +const getAppsCodefresh = async ({ + accessToken, +}: { + accessToken: string; +}) => { + const res = ( + await standardRequest.get(`${INTEGRATION_CODEFRESH_API_URL}/projects`, { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json", + }, + }) + ).data; + + const apps = res.projects.map((a: any) => ({ + name: a.projectName, + appId: a.id, + })); + + return apps; + +}; export { getApps }; diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index 0cc3a9438..93eaf22be 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -1,7 +1,7 @@ import _ from "lodash"; import AWS from "aws-sdk"; -import { - CreateSecretCommand, +import { + CreateSecretCommand, GetSecretValueCommand, ResourceNotFoundException, SecretsManagerClient, @@ -40,8 +40,10 @@ import { INTEGRATION_TRAVISCI_API_URL, INTEGRATION_VERCEL, INTEGRATION_VERCEL_API_URL, + INTEGRATION_CODEFRESH, + INTEGRATION_CODEFRESH_API_URL } from "../variables"; -import { standardRequest} from "../config/request"; +import { standardRequest } from "../config/request"; /** * Sync/push [secrets] to [app] in integration named [integration] @@ -163,64 +165,72 @@ const syncSecrets = async ({ break; case INTEGRATION_SUPABASE: await syncSecretsSupabase({ - integration, - secrets, - 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, - secrets, - accessToken, - }); - break; - case INTEGRATION_HASHICORP_VAULT: - await syncSecretsHashiCorpVault({ - integration, - integrationAuth, - secrets, - accessId, - accessToken, - }); - break; - case INTEGRATION_CLOUDFLARE_PAGES: - await syncSecretsCloudflarePages({ - integration, - secrets, - accessId, - accessToken - }); - break; - } + integration, + secrets, + 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, + secrets, + accessToken, + }); + break; + case INTEGRATION_HASHICORP_VAULT: + await syncSecretsHashiCorpVault({ + integration, + integrationAuth, + secrets, + accessId, + accessToken, + }); + break; + case INTEGRATION_CLOUDFLARE_PAGES: + await syncSecretsCloudflarePages({ + integration, + secrets, + accessId, + accessToken + }); + break; + case INTEGRATION_CODEFRESH: + await syncSecretsCodefresh({ + integration, + // integrationAuth, + secrets, + accessToken, + }); + break; + } }; /** @@ -249,11 +259,11 @@ const syncSecretsAzureKeyVault = async ({ recoverableDays: number; } } - + interface AzureKeyVaultSecret extends GetAzureKeyVaultSecret { key: string; } - + /** * Return all secrets from Azure Key Vault by paginating through URL [url] * @param {String} url - pagination URL to get next set of secrets from Azure Key Vault @@ -267,23 +277,23 @@ const syncSecretsAzureKeyVault = async ({ Authorization: `Bearer ${accessToken}`, }, }); - + result = result.concat(res.data.value); - + url = res.data.nextLink; } - + return result; } - + const getAzureKeyVaultSecrets = await paginateAzureKeyVaultSecrets(`${integration.app}/secrets?api-version=7.3`); - + let lastSlashIndex: number; const res = (await Promise.all(getAzureKeyVaultSecrets.map(async (getAzureKeyVaultSecret) => { if (!lastSlashIndex) { lastSlashIndex = getAzureKeyVaultSecret.id.lastIndexOf("/"); } - + const azureKeyVaultSecret = await standardRequest.get(`${getAzureKeyVaultSecret.id}?api-version=7.3`, { headers: { "Authorization": `Bearer ${accessToken}`, @@ -295,11 +305,11 @@ const syncSecretsAzureKeyVault = async ({ key: getAzureKeyVaultSecret.id.substring(lastSlashIndex + 1), }); }))) - .reduce((obj: any, secret: any) => ({ + .reduce((obj: any, secret: any) => ({ ...obj, [secret.key]: secret, - }), {}); - + }), {}); + const setSecrets: { key: string; value: string; @@ -323,9 +333,9 @@ const syncSecretsAzureKeyVault = async ({ } } }); - + const deleteSecrets: AzureKeyVaultSecret[] = []; - + Object.keys(res).forEach((key) => { const underscoredKey = key.replace(/-/g, "_"); if (!(underscoredKey in secrets)) { @@ -346,7 +356,7 @@ const syncSecretsAzureKeyVault = async ({ }) => { let isSecretSet = false; let maxTries = 6; - + while (!isSecretSet && maxTries > 0) { // try to set secret try { @@ -363,7 +373,7 @@ const syncSecretsAzureKeyVault = async ({ ); isSecretSet = true; - + } catch (err) { const error: any = err; if (error?.response?.data?.error?.innererror?.code === "ObjectIsDeletedButRecoverable") { @@ -383,7 +393,7 @@ const syncSecretsAzureKeyVault = async ({ } } } - + // Sync/push set secrets for await (const setSecret of setSecrets) { const { key, value } = setSecret; @@ -394,7 +404,7 @@ const syncSecretsAzureKeyVault = async ({ accessToken, }); } - + for await (const deleteSecret of deleteSecrets) { const { key } = deleteSecret; await standardRequest.delete(`${integration.app}/secrets/${key}?api-version=7.3`, { @@ -436,7 +446,7 @@ const syncSecretsAWSParameterStore = async ({ apiVersion: "2014-11-06", region: integration.region, }); - + const params = { Path: integration.path, Recursive: true, @@ -444,61 +454,61 @@ const syncSecretsAWSParameterStore = async ({ }; const parameterList = (await ssm.getParametersByPath(params).promise()).Parameters - + let awsParameterStoreSecretsObj: { [key: string]: any // TODO: fix type } = {}; if (parameterList) { awsParameterStoreSecretsObj = parameterList.reduce((obj: any, secret: any) => ({ - ...obj, - [secret.Name.split("/").pop()]: secret, + ...obj, + [secret.Name.split("/").pop()]: secret, }), {}); } // Identify secrets to create Object.keys(secrets).map(async (key) => { - if (!(key in awsParameterStoreSecretsObj)) { - // case: secret does not exist in AWS parameter store - // -> create secret + if (!(key in awsParameterStoreSecretsObj)) { + // case: secret does not exist in AWS parameter store + // -> create secret + await ssm.putParameter({ + Name: `${integration.path}${key}`, + Type: "SecureString", + Value: secrets[key], + Overwrite: true, + }).promise(); + } else { + // case: secret exists in AWS parameter store + + if (awsParameterStoreSecretsObj[key].Value !== secrets[key]) { + // case: secret value doesn't match one in AWS parameter store + // -> update secret await ssm.putParameter({ Name: `${integration.path}${key}`, Type: "SecureString", Value: secrets[key], Overwrite: true, }).promise(); - } else { - // case: secret exists in AWS parameter store - - if (awsParameterStoreSecretsObj[key].Value !== secrets[key]) { - // case: secret value doesn't match one in AWS parameter store - // -> update secret - await ssm.putParameter({ - Name: `${integration.path}${key}`, - Type: "SecureString", - Value: secrets[key], - Overwrite: true, - }).promise(); - } } + } }); // Identify secrets to delete Object.keys(awsParameterStoreSecretsObj).map(async (key) => { - if (!(key in secrets)) { - // case: - // -> delete secret - await ssm.deleteParameter({ - Name: awsParameterStoreSecretsObj[key].Name, - }).promise(); - } + if (!(key in secrets)) { + // case: + // -> delete secret + await ssm.deleteParameter({ + Name: awsParameterStoreSecretsObj[key].Name, + }).promise(); + } }); AWS.config.update({ region: undefined, accessKeyId: undefined, secretAccessKey: undefined, - }); + }); } /** @@ -529,7 +539,7 @@ const syncSecretsAWSSecretManager = async ({ accessKeyId: accessId, secretAccessKey: accessToken, }); - + secretsManager = new SecretsManagerClient({ region: integration.region, credentials: { @@ -543,13 +553,13 @@ const syncSecretsAWSSecretManager = async ({ SecretId: integration.app, }) ); - + let awsSecretManagerSecretObj: { [key: string]: any } = {}; - + if (awsSecretManagerSecret?.SecretString) { awsSecretManagerSecretObj = JSON.parse(awsSecretManagerSecret.SecretString); } - + if (!_.isEqual(awsSecretManagerSecretObj, secrets)) { await secretsManager.send(new UpdateSecretCommand({ SecretId: integration.app, @@ -561,19 +571,19 @@ const syncSecretsAWSSecretManager = async ({ region: undefined, accessKeyId: undefined, secretAccessKey: undefined, - }); + }); } catch (err) { if (err instanceof ResourceNotFoundException && secretsManager) { await secretsManager.send(new CreateSecretCommand({ Name: integration.app, SecretString: JSON.stringify(secrets), })); - } + } AWS.config.update({ region: undefined, accessKeyId: undefined, secretAccessKey: undefined, - }); + }); } } @@ -656,36 +666,36 @@ const syncSecretsVercel = async ({ decrypt: "true", ...(integrationAuth?.teamId ? { - teamId: integrationAuth.teamId, - } + teamId: integrationAuth.teamId, + } : {}), }; - + const vercelSecrets: VercelSecret[] = (await standardRequest.get( `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`, { params, headers: { - Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json", }, } )) - .data - .envs - .filter((secret: VercelSecret) => { - if (!secret.target.includes(integration.targetEnvironment)) { - // case: secret does not have the same target environment - return false; - } + .data + .envs + .filter((secret: VercelSecret) => { + if (!secret.target.includes(integration.targetEnvironment)) { + // case: secret does not have the same target environment + return false; + } - if (integration.targetEnvironment === "preview" && integration.path && integration.path !== secret.gitBranch) { - // case: secret on preview environment does not have same target git branch - return false; - } + if (integration.targetEnvironment === "preview" && integration.path && integration.path !== secret.gitBranch) { + // case: secret on preview environment does not have same target git branch + return false; + } - return true; - }); + return true; + }); // return secret.target.includes(integration.targetEnvironment); @@ -695,14 +705,14 @@ const syncSecretsVercel = async ({ if (vercelSecret.type === "encrypted") { // case: secret is encrypted -> need to decrypt const decryptedSecret = (await standardRequest.get( - `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${vercelSecret.id}`, - { - params, - headers: { - Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, - } + `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${vercelSecret.id}`, + { + params, + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json", + }, + } )).data; res[vercelSecret.key] = decryptedSecret; @@ -710,7 +720,7 @@ const syncSecretsVercel = async ({ res[vercelSecret.key] = vercelSecret; } } - + const updateSecrets: VercelSecret[] = []; const deleteSecrets: VercelSecret[] = []; const newSecrets: VercelSecret[] = []; @@ -741,9 +751,9 @@ const syncSecretsVercel = async ({ key: key, value: secrets[key], type: res[key].type, - target: res[key].target.includes(integration.targetEnvironment) - ? [...res[key].target] - : [...res[key].target, integration.targetEnvironment], + target: res[key].target.includes(integration.targetEnvironment) + ? [...res[key].target] + : [...res[key].target, integration.targetEnvironment], ...(integration.path ? { gitBranch: integration.path, } : {}), @@ -793,7 +803,7 @@ const syncSecretsVercel = async ({ }, } ); - } + } } for await (const secret of deleteSecrets) { @@ -806,7 +816,7 @@ const syncSecretsVercel = async ({ "Accept-Encoding": "application/json", }, } - ); + ); } }; @@ -1351,7 +1361,7 @@ const syncSecretsCircleCI = async ({ integration: IIntegration; secrets: any; accessToken: string; -}) => { +}) => { const circleciOrganizationDetail = ( await standardRequest.get(`${INTEGRATION_CIRCLECI_API_URL}/v2/me/collaborations`, { headers: { @@ -1438,13 +1448,13 @@ const syncSecretsTravisCI = async ({ } ) ) - .data - ?.env_vars - .reduce((obj: any, secret: any) => ({ + .data + ?.env_vars + .reduce((obj: any, secret: any) => ({ ...obj, [secret.name]: secret, - }), {}); - + }), {}); + // add secrets for await (const key of Object.keys(secrets)) { if (!(key in getSecretsRes)) { @@ -1489,7 +1499,7 @@ const syncSecretsTravisCI = async ({ } for await (const key of Object.keys(getSecretsRes)) { - if (!(key in secrets)){ + if (!(key in secrets)) { // delete secret await standardRequest.delete( `${INTEGRATION_TRAVISCI_API_URL}/settings/env_vars/${getSecretsRes[key].id}?repository_id=${getSecretsRes[key].repository_id}`, @@ -1534,29 +1544,29 @@ const syncSecretsGitLab = async ({ "Authorization": `Bearer ${accessToken}`, "Accept-Encoding": "application/json", }; - + let allEnvVariables: GitLabSecret[] = []; let url: string | null = `${gitLabApiUrl}?per_page=100`; - + while (url) { const response: any = await standardRequest.get(url, { headers }); allEnvVariables = [...allEnvVariables, ...response.data]; - + const linkHeader = response.headers.link; const nextLink = linkHeader?.split(",").find((part: string) => part.includes('rel="next"')); - + if (nextLink) { url = nextLink.trim().split(";")[0].slice(1, -1); } else { url = null; } } - + return allEnvVariables; }; const allEnvVariables = await getAllEnvVariables(integration?.appId, accessToken); - const getSecretsRes: GitLabSecret[] = allEnvVariables.filter((secret: GitLabSecret) => + const getSecretsRes: GitLabSecret[] = allEnvVariables.filter((secret: GitLabSecret) => secret.environment_scope === integration.targetEnvironment ); @@ -1638,8 +1648,8 @@ const syncSecretsSupabase = async ({ `${INTEGRATION_SUPABASE_API_URL}/v1/projects/${integration.appId}/secrets`, { headers: { - Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json", }, } ); @@ -1648,8 +1658,8 @@ const syncSecretsSupabase = async ({ const modifiedFormatForSecretInjection = Object.keys(secrets).map( (key) => { return { - name: key, - value: secrets[key], + name: key, + value: secrets[key], }; } ); @@ -1659,8 +1669,8 @@ const syncSecretsSupabase = async ({ modifiedFormatForSecretInjection, { headers: { - Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json", }, } ); @@ -1668,7 +1678,7 @@ const syncSecretsSupabase = async ({ const secretsToDelete: any = []; getSecretsRes?.forEach((secretObj: any) => { if (!(secretObj.name in secrets)) { - secretsToDelete.push(secretObj.name); + secretsToDelete.push(secretObj.name); } }); @@ -1715,18 +1725,18 @@ const syncSecretsCheckly = async ({ } ) ) - .data - .reduce((obj: any, secret: any) => ({ + .data + .reduce((obj: any, secret: any) => ({ ...obj, [secret.key]: secret.value, - }), {}); - + }), {}); + // add secrets for await (const key of Object.keys(secrets)) { if (!(key in getSecretsRes)) { // case: secret does not exist in checkly // -> add secret - + await standardRequest.post( `${INTEGRATION_CHECKLY_API_URL}/v1/variables`, { @@ -1745,7 +1755,7 @@ const syncSecretsCheckly = async ({ } else { // case: secret exists in checkly // -> update/set secret - + if (secrets[key] !== getSecretsRes[key]) { await standardRequest.put( `${INTEGRATION_CHECKLY_API_URL}/v1/variables/${key}`, @@ -1766,7 +1776,7 @@ const syncSecretsCheckly = async ({ } for await (const key of Object.keys(getSecretsRes)) { - if (!(key in secrets)){ + if (!(key in secrets)) { // delete secret await standardRequest.delete( `${INTEGRATION_CHECKLY_API_URL}/v1/variables/${key}`, @@ -1803,13 +1813,13 @@ const syncSecretsHashiCorpVault = async ({ accessToken: string; }) => { if (!accessId) return; - + interface LoginAppRoleRes { auth: { client_token: string; } } - + // get Vault client token (could be optimized) const { data }: { data: LoginAppRoleRes } = await standardRequest.post( `${integrationAuth.url}/v1/auth/approle/login`, @@ -1823,7 +1833,7 @@ const syncSecretsHashiCorpVault = async ({ }, } ); - + const clientToken = data.auth.client_token; await standardRequest.post( @@ -1851,46 +1861,46 @@ const syncSecretsHashiCorpVault = async ({ * @param {String} obj.accessToken - API token for Cloudflare */ const syncSecretsCloudflarePages = async ({ - integration, - secrets, - accessId, - accessToken, + integration, + secrets, + accessId, + accessToken, }: { - integration: IIntegration; - secrets: any; - accessId: string | null; - accessToken: string; + integration: IIntegration; + secrets: any; + accessId: string | null; + accessToken: string; }) => { // get secrets from cloudflare pages const getSecretsRes = ( - await standardRequest.get( - `${INTEGRATION_CLOUDFLARE_PAGES_API_URL}/client/v4/accounts/${accessId}/pages/projects/${integration.app}`, - { - headers: { - Authorization: `Bearer ${accessToken}`, - "Accept": "application/json", - }, - } - ) + await standardRequest.get( + `${INTEGRATION_CLOUDFLARE_PAGES_API_URL}/client/v4/accounts/${accessId}/pages/projects/${integration.app}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept": "application/json", + }, + } + ) ) - .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}; + const secretsObj: any = { ...secrets }; for (const [key, val] of Object.entries(secretsObj)) { - secretsObj[key] = { type: "secret_text", value: val }; + secretsObj[key] = { type: "secret_text", value: val }; } if (getSecretsRes) { - for await (const key of Object.keys(getSecretsRes)) { - if (!(key in secrets)) { - // case: secret does not exist in infisical - // -> delete secret from cloudflare pages - secretsObj[key] = null; - } + for await (const key of Object.keys(getSecretsRes)) { + if (!(key in secrets)) { + // case: secret does not exist in infisical + // -> delete secret from cloudflare pages + secretsObj[key] = null; } + } } const data = { @@ -1902,15 +1912,120 @@ const syncSecretsCloudflarePages = async ({ }; await standardRequest.patch( - `${INTEGRATION_CLOUDFLARE_PAGES_API_URL}/client/v4/accounts/${accessId}/pages/projects/${integration.app}`, - data, - { - headers: { - Authorization: `Bearer ${accessToken}`, - "Accept": "application/json", - }, - } + `${INTEGRATION_CLOUDFLARE_PAGES_API_URL}/client/v4/accounts/${accessId}/pages/projects/${integration.app}`, + data, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept": "application/json", + }, + } ); } + +/** + * Sync/push [secrets] to codefresh with name [integration.app] + * @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 Supabase integration + */ + + + +const syncSecretsCodefresh = async ({ + integration, + secrets, + accessToken, +}: { + integration: IIntegration; + secrets: any; + accessToken: string; +}) => { + // get variables from codefresh. + + // there is no endpoint to get secrets from codefresh + // const getSecretsRes = ( + // await standardRequest.get( + // `${INTEGRATION_CODEFRESH_API_URL}/api/pipelines/utils/extractVariables`, + // { + // headers: { + // "Authorization": `Bearer ${accessToken}`, + // "Accept-Encoding": "application/json", + // }, + // } + // ) + // ) + // .data + // .reduce((obj: any, secret: any) => ({ + // ...obj, + // [secret.key]: secret.value, + // }), {}); + + // add secrets + // for await (const key of Object.keys(secrets)) { + // if (!(key in getSecretsRes)) { + // // case: secret does not exist in codefresh + // // -> add secret + + // await standardRequest.post( + // `${INTEGRATION_CODEFRESH_API_URL}/api/pipelines`, + // { + // spec: { + // variables: [ + // { + // key, + // value: secrets[key], + // } + // ] + + // }, + // }, + // { + // headers: { + // "Authorization": `Bearer ${accessToken}`, + // "Accept": "application/json", + // "Content-Type": "application/json", + // }, + // } + // ); + // } else { + // // case: secret exists in checkly + // // -> update/set secret + + // if (secrets[key] !== getSecretsRes[key]) { + // await standardRequest.put( + // `${INTEGRATION_CODEFRESH_API_URL}/v1/variables/${key}`, + // { + // value: secrets[key], + // }, + // { + // headers: { + // "Authorization": `Bearer ${accessToken}`, + // "Content-Type": "application/json", + // "Accept": "application/json", + // }, + // } + // ); + // } + // } + // } + + // for await (const key of Object.keys(getSecretsRes)) { + // if (!(key in secrets)) { + // // delete secret + // await standardRequest.delete( + // `${INTEGRATION_CODEFRESH_API_URL}/v1/variables/${key}`, + // { + // headers: { + // "Authorization": `Bearer ${accessToken}`, + // "Accept": "application/json", + // }, + // } + // ); + // } + // } +}; + export { syncSecrets }; diff --git a/backend/src/models/integration.ts b/backend/src/models/integration.ts index d94cc9faa..642a82591 100644 --- a/backend/src/models/integration.ts +++ b/backend/src/models/integration.ts @@ -17,6 +17,7 @@ import { INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_TRAVISCI, INTEGRATION_VERCEL, + INTEGRATION_CODEFRESH } from "../variables"; export interface IIntegration { @@ -52,7 +53,8 @@ export interface IIntegration { | "supabase" | "checkly" | "hashicorp-vault" - | "cloudflare-pages"; + | "cloudflare-pages" + | "codefresh"; integrationAuth: Types.ObjectId; } @@ -141,6 +143,7 @@ const integrationSchema = new Schema( INTEGRATION_CHECKLY, INTEGRATION_HASHICORP_VAULT, INTEGRATION_CLOUDFLARE_PAGES, + INTEGRATION_CODEFRESH ], required: true, }, diff --git a/backend/src/models/integrationAuth.ts b/backend/src/models/integrationAuth.ts index 2d56c2063..8089800d7 100644 --- a/backend/src/models/integrationAuth.ts +++ b/backend/src/models/integrationAuth.ts @@ -19,12 +19,13 @@ import { INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_TRAVISCI, INTEGRATION_VERCEL, + INTEGRATION_CODEFRESH } 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' | 'codefresh'; teamId: string; accountId: string; url: string; @@ -69,6 +70,7 @@ const integrationAuthSchema = new Schema( INTEGRATION_SUPABASE, INTEGRATION_HASHICORP_VAULT, INTEGRATION_CLOUDFLARE_PAGES, + INTEGRATION_CODEFRESH ], required: true, }, diff --git a/backend/src/variables/integration.ts b/backend/src/variables/integration.ts index c87d2eb97..5eb50b583 100644 --- a/backend/src/variables/integration.ts +++ b/backend/src/variables/integration.ts @@ -25,6 +25,9 @@ export const INTEGRATION_SUPABASE = 'supabase'; export const INTEGRATION_CHECKLY = 'checkly'; export const INTEGRATION_HASHICORP_VAULT = 'hashicorp-vault'; export const INTEGRATION_CLOUDFLARE_PAGES = 'cloudflare-pages'; +// chisom +export const INTEGRATION_CODEFRESH = 'codefresh'; + export const INTEGRATION_SET = new Set([ INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_HEROKU, @@ -39,7 +42,8 @@ export const INTEGRATION_SET = new Set([ INTEGRATION_SUPABASE, INTEGRATION_CHECKLY, INTEGRATION_HASHICORP_VAULT, - INTEGRATION_CLOUDFLARE_PAGES + INTEGRATION_CLOUDFLARE_PAGES, + INTEGRATION_CODEFRESH ]); // integration types @@ -68,6 +72,7 @@ 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_CODEFRESH_API_URL = 'https://g.codefresh.io/api'; export const getIntegrationOptions = async () => { const INTEGRATION_OPTIONS = [ @@ -233,7 +238,16 @@ export const getIntegrationOptions = async () => { type: 'pat', clientId: '', docsLink: '' - } + }, + { + name: "Codefresh", + slug: "codefresh", + image: "Codefresh.png", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "", + }, ] return INTEGRATION_OPTIONS; diff --git a/frontend/public/images/integrations/codefresh.png b/frontend/public/images/integrations/codefresh.png new file mode 100644 index 0000000000000000000000000000000000000000..f2f181c11281dcb693e621d5459da50a2b119944 GIT binary patch literal 34956 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 literal 0 HcmV?d00001 diff --git a/frontend/src/hooks/api/integrationAuth/index.tsx b/frontend/src/hooks/api/integrationAuth/index.tsx index 3b18be83f..c9fda017f 100644 --- a/frontend/src/hooks/api/integrationAuth/index.tsx +++ b/frontend/src/hooks/api/integrationAuth/index.tsx @@ -5,5 +5,5 @@ export { useGetIntegrationAuthRailwayEnvironments, useGetIntegrationAuthRailwayServices, useGetIntegrationAuthTeams, - useGetIntegrationAuthVercelBranches + useGetIntegrationAuthVercelBranches, } from "./queries"; diff --git a/frontend/src/hooks/api/integrationAuth/queries.tsx b/frontend/src/hooks/api/integrationAuth/queries.tsx index aed885ff3..f90126009 100644 --- a/frontend/src/hooks/api/integrationAuth/queries.tsx +++ b/frontend/src/hooks/api/integrationAuth/queries.tsx @@ -19,6 +19,7 @@ const integrationAuthKeys = { integrationAuthId: string; appId: string; }) => [{ integrationAuthId, appId }, "integrationAuthVercelBranches"] as const, + getIntegrationAuthRailwayEnvironments: ({ integrationAuthId, appId @@ -64,6 +65,7 @@ const fetchIntegrationAuthTeams = async (integrationAuthId: string) => { return data.teams; }; + const fetchIntegrationAuthVercelBranches = async ({ integrationAuthId, appId @@ -224,6 +226,7 @@ export const useGetIntegrationAuthRailwayServices = ({ }); }; + export const useDeleteIntegrationAuth = () => { const queryClient = useQueryClient(); @@ -235,3 +238,4 @@ export const useDeleteIntegrationAuth = () => { } }); }; + diff --git a/frontend/src/pages/integrations/codefresh/authorize.tsx b/frontend/src/pages/integrations/codefresh/authorize.tsx new file mode 100644 index 000000000..aebfd2139 --- /dev/null +++ b/frontend/src/pages/integrations/codefresh/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 CodefreshCreateIntegrationPage() { + 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: "codefresh", + accessToken: apiKey, + accessId: null, + url: null, + namespace: null + }); + + setIsLoading(false); + + router.push(`/integrations/codefresh/create?integrationAuthId=${integrationAuth._id}`); + } catch (err) { + console.error(err); + } + }; + + return ( +
+ + Codefresh Integration + + setApiKey(e.target.value)} /> + + + +
+ ); +} + +CodefreshCreateIntegrationPage.requireAuth = true; diff --git a/frontend/src/pages/integrations/codefresh/create.tsx b/frontend/src/pages/integrations/codefresh/create.tsx new file mode 100644 index 000000000..214487b6e --- /dev/null +++ b/frontend/src/pages/integrations/codefresh/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 CodefreshCreateIntegrationPage() { + 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 ? ( +
+ + Codefresh Integration + + + + + setSecretPath(evt.target.value)} + placeholder="Provide a path, default is /" + /> + + + + + + +
+ ) : ( +
+ ); +} + +CodefreshCreateIntegrationPage.requireAuth = true; diff --git a/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx b/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx index fc60ead6d..9dc5be151 100644 --- a/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx +++ b/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx @@ -89,6 +89,10 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) => case "cloudflare-pages": link = `${window.location.origin}/integrations/cloudflare-pages/authorize`; break; + case "codefresh": + link = `${window.location.origin}/integrations/codefresh/authorize`; + break; + default: break; } diff --git a/frontend/src/views/IntegrationsPage/IntegrationsPage.tsx b/frontend/src/views/IntegrationsPage/IntegrationsPage.tsx index 5a585151d..e178406d2 100644 --- a/frontend/src/views/IntegrationsPage/IntegrationsPage.tsx +++ b/frontend/src/views/IntegrationsPage/IntegrationsPage.tsx @@ -49,6 +49,7 @@ export const IntegrationsPage = ({ frameworkIntegrations }: Props) => { const { data: cloudIntegrations, isLoading: isCloudIntegrationsLoading } = useGetCloudIntegrations(); + const { data: integrationAuths, isLoading: isIntegrationAuthLoading } = useGetWorkspaceAuthorizations( workspaceId, diff --git a/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx b/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx index e61c6fe47..65d1dc1ca 100644 --- a/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx +++ b/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx @@ -33,6 +33,7 @@ export const IntegrationsSection = ({ "deleteConfirmation" ] as const); + console.log(integrationSlugNameMapping, integrations, '>>>chisom') return (