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 000000000..f2f181c11 Binary files /dev/null and b/frontend/public/images/integrations/codefresh.png differ 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 (