diff --git a/backend/src/controllers/v1/integrationAuthController.ts b/backend/src/controllers/v1/integrationAuthController.ts index 70410ce35..ec166df48 100644 --- a/backend/src/controllers/v1/integrationAuthController.ts +++ b/backend/src/controllers/v1/integrationAuthController.ts @@ -10,6 +10,7 @@ import { ALGORITHM_AES_256_GCM, ENCODING_SCHEME_UTF8, INTEGRATION_BITBUCKET_API_URL, + INTEGRATION_CHECKLY_API_URL, INTEGRATION_GCP_SECRET_MANAGER, INTEGRATION_NORTHFLANK_API_URL, INTEGRATION_QOVERY_API_URL, @@ -344,6 +345,59 @@ export const getIntegrationAuthVercelBranches = async (req: Request, res: Respon }); }; +/** + * Return list of Checkly groups for a specific user + * @param req + * @param res + */ +export const getIntegrationAuthChecklyGroups = async (req: Request, res: Response) => { + const { + params: { integrationAuthId }, + query: { accountId } + } = await validateRequest(reqValidator.GetIntegrationAuthChecklyGroupsV1, req); + + const { integrationAuth, accessToken } = await getIntegrationAuthAccessHelper({ + integrationAuthId: new Types.ObjectId(integrationAuthId) + }); + + const { permission } = await getUserProjectPermissions( + req.user._id, + integrationAuth.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.Integrations + ); + + interface ChecklyGroup { + id: number; + name: string; + } + + if (accountId && accountId !== "") { + const { data }: { data: ChecklyGroup[] } = ( + await standardRequest.get(`${INTEGRATION_CHECKLY_API_URL}/v1/check-groups`, { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json", + "X-Checkly-Account": accountId + } + }) + ); + + return res.status(200).send({ + groups: data.map((g: ChecklyGroup) => ({ + name: g.name, + groupId: g.id, + })) + }); + } + + return res.status(200).send({ + groups: [] + }); +} + /** * Return list of Qovery Orgs for a specific user * @param req diff --git a/backend/src/integrations/apps.ts b/backend/src/integrations/apps.ts index e1aaf08e2..e67136275 100644 --- a/backend/src/integrations/apps.ts +++ b/backend/src/integrations/apps.ts @@ -911,7 +911,7 @@ const getAppsSupabase = async ({ accessToken }: { accessToken: string }) => { }; /** - * Return list of projects for the Checkly integration + * Return list of accounts for the Checkly integration * @param {Object} obj * @param {String} obj.accessToken - api key for the Checkly API * @returns {Object[]} apps - Сheckly accounts diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index 5f7d53345..1670651b2 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -2104,7 +2104,7 @@ const syncSecretsSupabase = async ({ }; /** - * Sync/push [secrets] to Checkly app + * Sync/push [secrets] to Checkly app/group * @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) @@ -2121,94 +2121,154 @@ const syncSecretsCheckly = async ({ accessToken: string; appendices?: { prefix: string; suffix: string }; }) => { - let getSecretsRes = ( - await standardRequest.get(`${INTEGRATION_CHECKLY_API_URL}/v1/variables`, { - headers: { - Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - "X-Checkly-Account": integration.appId - } - }) - ).data.reduce( - (obj: any, secret: any) => ({ - ...obj, - [secret.key]: secret.value - }), - {} - ); - getSecretsRes = Object.keys(getSecretsRes).reduce( - ( - result: { - [key: string]: string; - }, - key - ) => { - if ( - (appendices?.prefix !== undefined ? key.startsWith(appendices?.prefix) : true) && - (appendices?.suffix !== undefined ? key.endsWith(appendices?.suffix) : true) - ) { - result[key] = getSecretsRes[key]; - } - return result; - }, - {} - ); + if (integration.targetServiceId) { + // sync secrets to checkly group envars - // 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`, - { - key, - value: secrets[key].value - }, - { - headers: { - Authorization: `Bearer ${accessToken}`, - Accept: "application/json", - "Content-Type": "application/json", - "X-Checkly-Account": integration.appId - } - } - ); - } else { - // case: secret exists in checkly - // -> update/set secret - - if (secrets[key] !== getSecretsRes[key]) { - await standardRequest.put( - `${INTEGRATION_CHECKLY_API_URL}/v1/variables/${key}`, - { - value: secrets[key].value - }, - { - headers: { - Authorization: `Bearer ${accessToken}`, - "Content-Type": "application/json", - Accept: "application/json", - "X-Checkly-Account": integration.appId - } - } - ); - } - } - } - - for await (const key of Object.keys(getSecretsRes)) { - if (!(key in secrets)) { - // delete secret - await standardRequest.delete(`${INTEGRATION_CHECKLY_API_URL}/v1/variables/${key}`, { + let getGroupSecretsRes = ( + await standardRequest.get(`${INTEGRATION_CHECKLY_API_URL}/v1/check-groups/${integration.targetServiceId}`, { headers: { Authorization: `Bearer ${accessToken}`, Accept: "application/json", "X-Checkly-Account": integration.appId } - }); + }) + ).data.environmentVariables.reduce( + (obj: any, secret: any) => ({ + ...obj, + [secret.key]: secret.value + }), + {} + ); + + getGroupSecretsRes = Object.keys(getGroupSecretsRes).reduce( + ( + result: { + [key: string]: string; + }, + key + ) => { + if ( + (appendices?.prefix !== undefined ? key.startsWith(appendices?.prefix) : true) && + (appendices?.suffix !== undefined ? key.endsWith(appendices?.suffix) : true) + ) { + result[key] = getGroupSecretsRes[key]; + } + return result; + }, + {} + ); + + const groupEnvironmentVariables = Object.keys(secrets).map(key => ({ + key, + value: secrets[key].value + })); + + await standardRequest.put( + `${INTEGRATION_CHECKLY_API_URL}/v1/check-groups/${integration.targetServiceId}`, + { + environmentVariables: groupEnvironmentVariables + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json", + "X-Checkly-Account": integration.appId + } + } + ); + } else { + // sync secrets to checkly global envars + + let getSecretsRes = ( + await standardRequest.get(`${INTEGRATION_CHECKLY_API_URL}/v1/variables`, { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json", + "X-Checkly-Account": integration.appId + } + }) + ).data.reduce( + (obj: any, secret: any) => ({ + ...obj, + [secret.key]: secret.value + }), + {} + ); + + getSecretsRes = Object.keys(getSecretsRes).reduce( + ( + result: { + [key: string]: string; + }, + key + ) => { + if ( + (appendices?.prefix !== undefined ? key.startsWith(appendices?.prefix) : true) && + (appendices?.suffix !== undefined ? key.endsWith(appendices?.suffix) : true) + ) { + result[key] = getSecretsRes[key]; + } + return result; + }, + {} + ); + + // 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`, + { + key, + value: secrets[key].value + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json", + "Content-Type": "application/json", + "X-Checkly-Account": integration.appId + } + } + ); + } else { + // case: secret exists in checkly + // -> update/set secret + + if (secrets[key] !== getSecretsRes[key]) { + await standardRequest.put( + `${INTEGRATION_CHECKLY_API_URL}/v1/variables/${key}`, + { + value: secrets[key].value + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + Accept: "application/json", + "X-Checkly-Account": integration.appId + } + } + ); + } + } } + + for await (const key of Object.keys(getSecretsRes)) { + if (!(key in secrets)) { + // delete secret + await standardRequest.delete(`${INTEGRATION_CHECKLY_API_URL}/v1/variables/${key}`, { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json", + "X-Checkly-Account": integration.appId + } + }); + } + } } }; diff --git a/backend/src/routes/v1/integrationAuth.ts b/backend/src/routes/v1/integrationAuth.ts index e28874788..68d0433b3 100644 --- a/backend/src/routes/v1/integrationAuth.ts +++ b/backend/src/routes/v1/integrationAuth.ts @@ -60,6 +60,14 @@ router.get( integrationAuthController.getIntegrationAuthVercelBranches ); +router.get( + "/:integrationAuthId/checkly/groups", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + integrationAuthController.getIntegrationAuthChecklyGroups +); + router.get( "/:integrationAuthId/qovery/orgs", requireAuth({ diff --git a/backend/src/services/RedisService.ts b/backend/src/services/RedisService.ts index 23f945bb7..218234a1a 100644 --- a/backend/src/services/RedisService.ts +++ b/backend/src/services/RedisService.ts @@ -1,11 +1,12 @@ import Redis, { Redis as TRedis } from "ioredis"; +import { logger } from "../utils/logging"; let redisClient: TRedis | null; if (process.env.REDIS_URL) { redisClient = new Redis(process.env.REDIS_URL as string); } else { - console.warn("Redis URL not set, skipping Redis initialization."); + logger.warn("Redis URL not set, skipping Redis initialization."); redisClient = null; } diff --git a/backend/src/utils/setup/index.ts b/backend/src/utils/setup/index.ts index 6625bff7b..62c7272f3 100644 --- a/backend/src/utils/setup/index.ts +++ b/backend/src/utils/setup/index.ts @@ -11,7 +11,6 @@ import { backfillBots, backfillEncryptionMetadata, backfillIntegration, - backfillPermission, backfillSecretBlindIndexData, backfillSecretFolders, backfillSecretVersions, @@ -28,6 +27,7 @@ import { } from "./reencryptData"; import { getMongoURL, getNodeEnv, getRedisUrl, getSentryDSN } from "../../config"; import { initializePassport } from "../auth"; +import { logger } from "../logging"; /** * Prepare Infisical upon startup. This includes tasks like: @@ -41,7 +41,7 @@ import { initializePassport } from "../auth"; */ export const setup = async () => { if ((await getRedisUrl()) === undefined || (await getRedisUrl()) === "") { - console.error( + logger.error( "WARNING: Redis is not yet configured. Infisical may not function as expected without it." ); } diff --git a/backend/src/validation/integrationAuth.ts b/backend/src/validation/integrationAuth.ts index 65eee4477..3eb51d833 100644 --- a/backend/src/validation/integrationAuth.ts +++ b/backend/src/validation/integrationAuth.ts @@ -117,6 +117,15 @@ export const GetIntegrationAuthVercelBranchesV1 = z.object({ }) }); +export const GetIntegrationAuthChecklyGroupsV1 = z.object({ + params: z.object({ + integrationAuthId: z.string().trim() + }), + query: z.object({ + accountId: z.string().trim() + }) +}); + export const GetIntegrationAuthQoveryOrgsV1 = z.object({ params: z.object({ integrationAuthId: z.string().trim() diff --git a/docs/images/integrations/checkly/integrations-checkly-auth.png b/docs/images/integrations/checkly/integrations-checkly-auth.png index b6299aefe..b9fbd4404 100644 Binary files a/docs/images/integrations/checkly/integrations-checkly-auth.png and b/docs/images/integrations/checkly/integrations-checkly-auth.png differ diff --git a/docs/images/integrations/checkly/integrations-checkly-create.png b/docs/images/integrations/checkly/integrations-checkly-create.png index 271ca1a9c..78faa5eac 100644 Binary files a/docs/images/integrations/checkly/integrations-checkly-create.png and b/docs/images/integrations/checkly/integrations-checkly-create.png differ diff --git a/docs/images/integrations/checkly/integrations-checkly.png b/docs/images/integrations/checkly/integrations-checkly.png index 4734d5257..9dcb4e497 100644 Binary files a/docs/images/integrations/checkly/integrations-checkly.png and b/docs/images/integrations/checkly/integrations-checkly.png differ diff --git a/docs/integrations/cloud/checkly.mdx b/docs/integrations/cloud/checkly.mdx index 711c8cec9..6b4709be9 100644 --- a/docs/integrations/cloud/checkly.mdx +++ b/docs/integrations/cloud/checkly.mdx @@ -34,6 +34,13 @@ Press on the Checkly tile and input your Checkly API Key to grant Infisical acce Select which Infisical environment secrets you want to sync to Checkly and press create integration to start syncing secrets. ![integrations checkly](../../images/integrations/checkly/integrations-checkly-create.png) + + + Infisical integrates with Checkly's environment variables at the **global** and **group** levels. + + To sync secrets to a specific group, you can select a group from the Checkly Group dropdown; otherwise, leaving it empty will sync secrets globally. + + ![integrations checkly](../../images/integrations/checkly/integrations-checkly.png) diff --git a/frontend/src/hooks/api/integrationAuth/index.tsx b/frontend/src/hooks/api/integrationAuth/index.tsx index 7e7c355ca..f5065919f 100644 --- a/frontend/src/hooks/api/integrationAuth/index.tsx +++ b/frontend/src/hooks/api/integrationAuth/index.tsx @@ -4,6 +4,7 @@ export { useGetIntegrationAuthApps, useGetIntegrationAuthBitBucketWorkspaces, useGetIntegrationAuthById, + useGetIntegrationAuthChecklyGroups, useGetIntegrationAuthNorthflankSecretGroups, useGetIntegrationAuthRailwayEnvironments, useGetIntegrationAuthRailwayServices, @@ -11,4 +12,4 @@ export { useGetIntegrationAuthTeams, useGetIntegrationAuthVercelBranches, useSaveIntegrationAccessToken -} from "./queries"; +} from "./queries"; \ No newline at end of file diff --git a/frontend/src/hooks/api/integrationAuth/queries.tsx b/frontend/src/hooks/api/integrationAuth/queries.tsx index 9213919e0..7e5d3d711 100644 --- a/frontend/src/hooks/api/integrationAuth/queries.tsx +++ b/frontend/src/hooks/api/integrationAuth/queries.tsx @@ -6,14 +6,16 @@ import { workspaceKeys } from "../workspace/queries"; import { App, BitBucketWorkspace, + ChecklyGroup, Environment, IntegrationAuth, NorthflankSecretGroup, Org, Project, Service, - Team, - TeamCityBuildConfig} from "./types"; + Team, + TeamCityBuildConfig +} from "./types"; const integrationAuthKeys = { getIntegrationAuthById: (integrationAuthId: string) => @@ -29,6 +31,14 @@ const integrationAuthKeys = { integrationAuthId: string; appId: string; }) => [{ integrationAuthId, appId }, "integrationAuthVercelBranches"] as const, + getIntegrationAuthChecklyGroups: ({ + integrationAuthId, + accountId + }: { + integrationAuthId: string; + accountId: string; + }) => + [{ integrationAuthId, accountId }, "integrationAuthChecklyGroups"] as const, getIntegrationAuthQoveryOrgs: (integrationAuthId: string) => [{ integrationAuthId }, "integrationAuthQoveryOrgs"] as const, getIntegrationAuthQoveryProjects: ({ @@ -125,6 +135,24 @@ const fetchIntegrationAuthTeams = async (integrationAuthId: string) => { return data.teams; }; +const fetchIntegrationAuthChecklyGroups = async ({ + integrationAuthId, + accountId +}: { + integrationAuthId: string; + accountId: string; +}) => { + const { data } = await apiRequest.get<{ groups: ChecklyGroup[] }>( + `/api/v1/integration-auth/${integrationAuthId}/checkly/groups`, + { + params: { + accountId + } + } + ); + + return data.groups; +}; const fetchIntegrationAuthVercelBranches = async ({ integrationAuthId, @@ -413,6 +441,26 @@ export const useGetIntegrationAuthVercelBranches = ({ }); }; +export const useGetIntegrationAuthChecklyGroups = ({ + integrationAuthId, + accountId +}: { + integrationAuthId: string; + accountId: string; +}) => { + return useQuery({ + queryKey: integrationAuthKeys.getIntegrationAuthChecklyGroups({ + integrationAuthId, + accountId + }), + queryFn: () => fetchIntegrationAuthChecklyGroups({ + integrationAuthId, + accountId + }), + enabled: true + }); +}; + export const useGetIntegrationAuthQoveryOrgs = (integrationAuthId: string) => { return useQuery({ queryKey: integrationAuthKeys.getIntegrationAuthQoveryOrgs(integrationAuthId), diff --git a/frontend/src/hooks/api/integrationAuth/types.ts b/frontend/src/hooks/api/integrationAuth/types.ts index 2292e3222..5af8a3773 100644 --- a/frontend/src/hooks/api/integrationAuth/types.ts +++ b/frontend/src/hooks/api/integrationAuth/types.ts @@ -26,6 +26,11 @@ export type Environment = { environmentId: string; }; +export type ChecklyGroup = { + name: string; + groupId: number; +}; + export type Container = { name: string; containerId: string; diff --git a/frontend/src/pages/integrations/checkly/create.tsx b/frontend/src/pages/integrations/checkly/create.tsx index 4393dfc0e..4db24b846 100644 --- a/frontend/src/pages/integrations/checkly/create.tsx +++ b/frontend/src/pages/integrations/checkly/create.tsx @@ -3,7 +3,7 @@ import Head from "next/head"; import Image from "next/image"; import Link from "next/link"; import { useRouter } from "next/router"; -import { faArrowUpRightFromSquare, faBookOpen, faBugs, faCircleInfo } from "@fortawesome/free-solid-svg-icons"; +import { faArrowUpRightFromSquare, faBookOpen, faBugs } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { motion } from "framer-motion"; import queryString from "query-string"; @@ -27,7 +27,8 @@ import { import { useGetIntegrationAuthApps, - useGetIntegrationAuthById + useGetIntegrationAuthById, + useGetIntegrationAuthChecklyGroups } from "../../../hooks/api/integrationAuth"; import { useGetWorkspaceById } from "../../../hooks/api/workspace"; @@ -42,20 +43,24 @@ export default function ChecklyCreateIntegrationPage() { const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]); + const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(""); + const [secretPath, setSecretPath] = useState("/"); + const [secretSuffix, setSecretSuffix] = useState(""); + + const [targetAppId, setTargetAppId] = useState(""); + const [targetGroupId, setTargetGroupId] = useState(""); + + const [isLoading, setIsLoading] = useState(false); + const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? ""); const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? ""); const { data: integrationAuthApps, isLoading: isIntegrationAuthAppsLoading } = useGetIntegrationAuthApps({ integrationAuthId: (integrationAuthId as string) ?? "" }); - - const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(""); - const [secretPath, setSecretPath] = useState("/"); - const [secretSuffix, setSecretSuffix] = useState(""); - - const [targetApp, setTargetApp] = useState(""); - const [targetAppId, setTargetAppId] = useState(""); - - const [isLoading, setIsLoading] = useState(false); + const { data: integrationAuthGroups, isLoading: isintegrationAuthGroupsLoading } = useGetIntegrationAuthChecklyGroups({ + integrationAuthId: (integrationAuthId as string) ?? "", + accountId: targetAppId + }); useEffect(() => { if (workspace) { @@ -64,13 +69,11 @@ export default function ChecklyCreateIntegrationPage() { }, [workspace]); useEffect(() => { - // TODO: handle case where apps can be empty if (integrationAuthApps) { if (integrationAuthApps.length > 0) { - setTargetApp(integrationAuthApps[0].name); - setTargetAppId(String(integrationAuthApps[0].appId)); + setTargetAppId(integrationAuthApps[0].appId as string); } else { - setTargetApp("none"); + setTargetAppId("none"); } } }, [integrationAuthApps]); @@ -81,12 +84,23 @@ export default function ChecklyCreateIntegrationPage() { setIsLoading(true); + const targetApp = integrationAuthApps?.find( + (integrationAuthApp) => integrationAuthApp.appId === targetAppId + ); + const targetGroup = integrationAuthGroups?.find( + (group) => group.groupId === Number(targetGroupId) + ); + + if (!targetApp) return; + await mutateAsync({ integrationAuthId: integrationAuth?._id, isActive: true, - app: targetApp, - appId: targetAppId, + app: targetApp?.name, + appId: targetApp?.appId, sourceEnvironment: selectedSourceEnvironment, + targetService: targetGroup?.name, + targetServiceId: String(targetGroup?.groupId), secretPath, metadata: { secretSuffix @@ -104,9 +118,10 @@ export default function ChecklyCreateIntegrationPage() { return integrationAuth && workspace && selectedSourceEnvironment && - integrationAuthApps && - targetApp ? ( -
+ integrationAuthApps && + integrationAuthGroups && + targetAppId ? ( +
Set Up Checkly Integration @@ -177,16 +192,16 @@ export default function ChecklyCreateIntegrationPage() { + + + @@ -229,12 +266,6 @@ export default function ChecklyCreateIntegrationPage() { Create Integration -
-
-
Pro Tips
- After creating an integration, your secrets will start syncing immediately. This might cause an unexpected override of current secrets in Checkly with secrets from Infisical. - If you have multiple Checkly integrations and are using suffixes for at least one of them, you will have to add suffixes for all the active Checkly integrations – otherwise you might run into rare unexpected behavior. -
) : (
@@ -242,7 +273,7 @@ export default function ChecklyCreateIntegrationPage() { Set Up Checkly Integration - {isIntegrationAuthAppsLoading ? infisical loading indicator :