mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge pull request #3049 from Infisical/daniel/vercel-custom-envs
feat(integrations/vercel): custom environments support
This commit is contained in:
@@ -1151,6 +1151,50 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:integrationAuthId/vercel/custom-environments",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
schema: {
|
||||||
|
querystring: z.object({
|
||||||
|
teamId: z.string().trim()
|
||||||
|
}),
|
||||||
|
params: z.object({
|
||||||
|
integrationAuthId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
environments: z
|
||||||
|
.object({
|
||||||
|
appId: z.string(),
|
||||||
|
customEnvironments: z
|
||||||
|
.object({
|
||||||
|
id: z.string(),
|
||||||
|
slug: z.string()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const environments = await server.services.integrationAuth.getVercelCustomEnvironments({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
id: req.params.integrationAuthId,
|
||||||
|
teamId: req.query.teamId
|
||||||
|
});
|
||||||
|
|
||||||
|
return { environments };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/:integrationAuthId/octopus-deploy/spaces",
|
url: "/:integrationAuthId/octopus-deploy/spaces",
|
||||||
|
|||||||
@@ -132,16 +132,26 @@ const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Return list of names of apps for Vercel integration
|
* Return list of names of apps for Vercel integration
|
||||||
|
* This is re-used for getting custom environments for Vercel
|
||||||
*/
|
*/
|
||||||
const getAppsVercel = async ({ accessToken, teamId }: { teamId?: string | null; accessToken: string }) => {
|
export const getAppsVercel = async ({ accessToken, teamId }: { teamId?: string | null; accessToken: string }) => {
|
||||||
const apps: Array<{ name: string; appId: string }> = [];
|
const apps: Array<{ name: string; appId: string; customEnvironments: Array<{ slug: string; id: string }> }> = [];
|
||||||
|
|
||||||
const limit = "20";
|
const limit = "20";
|
||||||
let hasMorePages = true;
|
let hasMorePages = true;
|
||||||
let next: number | null = null;
|
let next: number | null = null;
|
||||||
|
|
||||||
interface Response {
|
interface Response {
|
||||||
projects: { name: string; id: string }[];
|
projects: {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
customEnvironments?: {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
description: string;
|
||||||
|
slug: string;
|
||||||
|
}[];
|
||||||
|
}[];
|
||||||
pagination: {
|
pagination: {
|
||||||
count: number;
|
count: number;
|
||||||
next: number | null;
|
next: number | null;
|
||||||
@@ -173,7 +183,12 @@ const getAppsVercel = async ({ accessToken, teamId }: { teamId?: string | null;
|
|||||||
data.projects.forEach((a) => {
|
data.projects.forEach((a) => {
|
||||||
apps.push({
|
apps.push({
|
||||||
name: a.name,
|
name: a.name,
|
||||||
appId: a.id
|
appId: a.id,
|
||||||
|
customEnvironments:
|
||||||
|
a.customEnvironments?.map((env) => ({
|
||||||
|
slug: env.slug,
|
||||||
|
id: env.id
|
||||||
|
})) ?? []
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -25,11 +25,12 @@ import { TIntegrationDALFactory } from "../integration/integration-dal";
|
|||||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
import { KmsDataKey } from "../kms/kms-types";
|
import { KmsDataKey } from "../kms/kms-types";
|
||||||
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
|
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
|
||||||
import { getApps } from "./integration-app-list";
|
import { getApps, getAppsVercel } from "./integration-app-list";
|
||||||
import { TCircleCIContext } from "./integration-app-types";
|
import { TCircleCIContext } from "./integration-app-types";
|
||||||
import { TIntegrationAuthDALFactory } from "./integration-auth-dal";
|
import { TIntegrationAuthDALFactory } from "./integration-auth-dal";
|
||||||
import { IntegrationAuthMetadataSchema, TIntegrationAuthMetadata } from "./integration-auth-schema";
|
import { IntegrationAuthMetadataSchema, TIntegrationAuthMetadata } from "./integration-auth-schema";
|
||||||
import {
|
import {
|
||||||
|
GetVercelCustomEnvironmentsDTO,
|
||||||
OctopusDeployScope,
|
OctopusDeployScope,
|
||||||
TBitbucketEnvironment,
|
TBitbucketEnvironment,
|
||||||
TBitbucketWorkspace,
|
TBitbucketWorkspace,
|
||||||
@@ -1825,6 +1826,41 @@ export const integrationAuthServiceFactory = ({
|
|||||||
return integrationAuthDAL.create(newIntegrationAuth);
|
return integrationAuthDAL.create(newIntegrationAuth);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getVercelCustomEnvironments = async ({
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
teamId,
|
||||||
|
id
|
||||||
|
}: GetVercelCustomEnvironmentsDTO) => {
|
||||||
|
const integrationAuth = await integrationAuthDAL.findById(id);
|
||||||
|
if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${id}' not found` });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId: integrationAuth.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
|
});
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
|
||||||
|
|
||||||
|
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(integrationAuth.projectId);
|
||||||
|
const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
|
||||||
|
|
||||||
|
const vercelApps = await getAppsVercel({
|
||||||
|
accessToken,
|
||||||
|
teamId
|
||||||
|
});
|
||||||
|
|
||||||
|
return vercelApps.map((app) => ({
|
||||||
|
customEnvironments: app.customEnvironments,
|
||||||
|
appId: app.appId
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
const getOctopusDeploySpaces = async ({
|
const getOctopusDeploySpaces = async ({
|
||||||
actorId,
|
actorId,
|
||||||
actor,
|
actor,
|
||||||
@@ -1944,6 +1980,7 @@ export const integrationAuthServiceFactory = ({
|
|||||||
getIntegrationAccessToken,
|
getIntegrationAccessToken,
|
||||||
duplicateIntegrationAuth,
|
duplicateIntegrationAuth,
|
||||||
getOctopusDeploySpaces,
|
getOctopusDeploySpaces,
|
||||||
getOctopusDeployScopeValues
|
getOctopusDeployScopeValues,
|
||||||
|
getVercelCustomEnvironments
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -284,3 +284,8 @@ export type TOctopusDeployVariableSet = {
|
|||||||
Self: string;
|
Self: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GetVercelCustomEnvironmentsDTO = {
|
||||||
|
teamId: string;
|
||||||
|
id: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|||||||
@@ -1450,9 +1450,13 @@ const syncSecretsVercel = async ({
|
|||||||
secrets: Record<string, { value: string; comment?: string } | null>;
|
secrets: Record<string, { value: string; comment?: string } | null>;
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
}) => {
|
}) => {
|
||||||
|
const isCustomEnvironment = !["development", "preview", "production"].includes(
|
||||||
|
integration.targetEnvironment as string
|
||||||
|
);
|
||||||
interface VercelSecret {
|
interface VercelSecret {
|
||||||
id?: string;
|
id?: string;
|
||||||
type: string;
|
type: string;
|
||||||
|
customEnvironmentIds?: string[];
|
||||||
key: string;
|
key: string;
|
||||||
value: string;
|
value: string;
|
||||||
target: string[];
|
target: string[];
|
||||||
@@ -1486,6 +1490,16 @@ const syncSecretsVercel = async ({
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
).data.envs.filter((secret) => {
|
).data.envs.filter((secret) => {
|
||||||
|
if (isCustomEnvironment) {
|
||||||
|
if (!secret.customEnvironmentIds?.includes(integration.targetEnvironment as string)) {
|
||||||
|
// case: secret does not have the same custom environment
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// no need to check for preview environment, as custom environments are not available in preview
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (!secret.target.includes(integration.targetEnvironment as string)) {
|
if (!secret.target.includes(integration.targetEnvironment as string)) {
|
||||||
// case: secret does not have the same target environment
|
// case: secret does not have the same target environment
|
||||||
return false;
|
return false;
|
||||||
@@ -1583,7 +1597,13 @@ const syncSecretsVercel = async ({
|
|||||||
key,
|
key,
|
||||||
value: infisicalSecrets[key]?.value,
|
value: infisicalSecrets[key]?.value,
|
||||||
type: "encrypted",
|
type: "encrypted",
|
||||||
target: [integration.targetEnvironment as string],
|
...(isCustomEnvironment
|
||||||
|
? {
|
||||||
|
customEnvironmentIds: [integration.targetEnvironment as string]
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
target: [integration.targetEnvironment as string]
|
||||||
|
}),
|
||||||
...(integration.path
|
...(integration.path
|
||||||
? {
|
? {
|
||||||
gitBranch: integration.path
|
gitBranch: integration.path
|
||||||
@@ -1607,9 +1627,19 @@ const syncSecretsVercel = async ({
|
|||||||
key,
|
key,
|
||||||
value: infisicalSecrets[key]?.value,
|
value: infisicalSecrets[key]?.value,
|
||||||
type: res[key].type,
|
type: res[key].type,
|
||||||
target: res[key].target.includes(integration.targetEnvironment as string)
|
|
||||||
? [...res[key].target]
|
...(!isCustomEnvironment
|
||||||
: [...res[key].target, integration.targetEnvironment as string],
|
? {
|
||||||
|
target: res[key].target.includes(integration.targetEnvironment as string)
|
||||||
|
? [...res[key].target]
|
||||||
|
: [...res[key].target, integration.targetEnvironment as string]
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
customEnvironmentIds: res[key].customEnvironmentIds?.includes(integration.targetEnvironment as string)
|
||||||
|
? [...(res[key].customEnvironmentIds || [])]
|
||||||
|
: [...(res[key]?.customEnvironmentIds || []), integration.targetEnvironment as string]
|
||||||
|
}),
|
||||||
|
|
||||||
...(integration.path
|
...(integration.path
|
||||||
? {
|
? {
|
||||||
gitBranch: integration.path
|
gitBranch: integration.path
|
||||||
|
|||||||
@@ -16,5 +16,6 @@ export {
|
|||||||
useGetIntegrationAuthTeamCityBuildConfigs,
|
useGetIntegrationAuthTeamCityBuildConfigs,
|
||||||
useGetIntegrationAuthTeams,
|
useGetIntegrationAuthTeams,
|
||||||
useGetIntegrationAuthVercelBranches,
|
useGetIntegrationAuthVercelBranches,
|
||||||
|
useGetIntegrationAuthVercelCustomEnvironments,
|
||||||
useSaveIntegrationAccessToken
|
useSaveIntegrationAccessToken
|
||||||
} from "./queries";
|
} from "./queries";
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ import {
|
|||||||
Team,
|
Team,
|
||||||
TeamCityBuildConfig,
|
TeamCityBuildConfig,
|
||||||
TGetIntegrationAuthOctopusDeployScopeValuesDTO,
|
TGetIntegrationAuthOctopusDeployScopeValuesDTO,
|
||||||
TOctopusDeployVariableSetScopeValues
|
TOctopusDeployVariableSetScopeValues,
|
||||||
|
VercelEnvironment
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
const integrationAuthKeys = {
|
const integrationAuthKeys = {
|
||||||
@@ -132,7 +133,9 @@ const integrationAuthKeys = {
|
|||||||
}: TGetIntegrationAuthOctopusDeployScopeValuesDTO) =>
|
}: TGetIntegrationAuthOctopusDeployScopeValuesDTO) =>
|
||||||
[{ integrationAuthId }, "getIntegrationAuthOctopusDeployScopeValues", params] as const,
|
[{ integrationAuthId }, "getIntegrationAuthOctopusDeployScopeValues", params] as const,
|
||||||
getIntegrationAuthCircleCIOrganizations: (integrationAuthId: string) =>
|
getIntegrationAuthCircleCIOrganizations: (integrationAuthId: string) =>
|
||||||
[{ integrationAuthId }, "getIntegrationAuthCircleCIOrganizations"] as const
|
[{ integrationAuthId }, "getIntegrationAuthCircleCIOrganizations"] as const,
|
||||||
|
getIntegrationAuthVercelCustomEnv: (integrationAuthId: string, teamId: string) =>
|
||||||
|
[{ integrationAuthId, teamId }, "integrationAuthVercelCustomEnv"] as const
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchIntegrationAuthById = async (integrationAuthId: string) => {
|
const fetchIntegrationAuthById = async (integrationAuthId: string) => {
|
||||||
@@ -362,6 +365,29 @@ const fetchIntegrationAuthQoveryScopes = async ({
|
|||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchIntegrationAuthVercelCustomEnvironments = async ({
|
||||||
|
integrationAuthId,
|
||||||
|
teamId
|
||||||
|
}: {
|
||||||
|
integrationAuthId: string;
|
||||||
|
teamId: string;
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
data: { environments }
|
||||||
|
} = await apiRequest.get<{
|
||||||
|
environments: {
|
||||||
|
appId: string;
|
||||||
|
customEnvironments: VercelEnvironment[];
|
||||||
|
}[];
|
||||||
|
}>(`/api/v1/integration-auth/${integrationAuthId}/vercel/custom-environments`, {
|
||||||
|
params: {
|
||||||
|
teamId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return environments;
|
||||||
|
};
|
||||||
|
|
||||||
const fetchIntegrationAuthHerokuPipelines = async ({
|
const fetchIntegrationAuthHerokuPipelines = async ({
|
||||||
integrationAuthId
|
integrationAuthId
|
||||||
}: {
|
}: {
|
||||||
@@ -730,6 +756,24 @@ export const useGetIntegrationAuthQoveryScopes = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useGetIntegrationAuthVercelCustomEnvironments = ({
|
||||||
|
integrationAuthId,
|
||||||
|
teamId
|
||||||
|
}: {
|
||||||
|
integrationAuthId: string;
|
||||||
|
teamId: string;
|
||||||
|
}) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: integrationAuthKeys.getIntegrationAuthVercelCustomEnv(integrationAuthId, teamId),
|
||||||
|
queryFn: () =>
|
||||||
|
fetchIntegrationAuthVercelCustomEnvironments({
|
||||||
|
integrationAuthId,
|
||||||
|
teamId
|
||||||
|
}),
|
||||||
|
enabled: Boolean(teamId && integrationAuthId)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const useGetIntegrationAuthHerokuPipelines = ({
|
export const useGetIntegrationAuthHerokuPipelines = ({
|
||||||
integrationAuthId
|
integrationAuthId
|
||||||
}: {
|
}: {
|
||||||
|
|||||||
@@ -43,6 +43,11 @@ export type Environment = {
|
|||||||
environmentId: string;
|
environmentId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type VercelEnvironment = {
|
||||||
|
id: string;
|
||||||
|
slug: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type ChecklyGroup = {
|
export type ChecklyGroup = {
|
||||||
name: string;
|
name: string;
|
||||||
groupId: number;
|
groupId: number;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
import {
|
import {
|
||||||
faArrowUpRightFromSquare,
|
faArrowUpRightFromSquare,
|
||||||
@@ -25,7 +25,8 @@ import { useCreateIntegration } from "@app/hooks/api";
|
|||||||
import {
|
import {
|
||||||
useGetIntegrationAuthApps,
|
useGetIntegrationAuthApps,
|
||||||
useGetIntegrationAuthById,
|
useGetIntegrationAuthById,
|
||||||
useGetIntegrationAuthVercelBranches
|
useGetIntegrationAuthVercelBranches,
|
||||||
|
useGetIntegrationAuthVercelCustomEnvironments
|
||||||
} from "@app/hooks/api/integrationAuth";
|
} from "@app/hooks/api/integrationAuth";
|
||||||
import { IntegrationSyncBehavior } from "@app/hooks/api/integrations/types";
|
import { IntegrationSyncBehavior } from "@app/hooks/api/integrations/types";
|
||||||
|
|
||||||
@@ -75,6 +76,11 @@ export const VercelConfigurePage = () => {
|
|||||||
teamId: integrationAuth?.teamId as string
|
teamId: integrationAuth?.teamId as string
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data: customEnvironments } = useGetIntegrationAuthVercelCustomEnvironments({
|
||||||
|
teamId: integrationAuth?.teamId as string,
|
||||||
|
integrationAuthId: integrationAuthId as string
|
||||||
|
});
|
||||||
|
|
||||||
const { data: branches } = useGetIntegrationAuthVercelBranches({
|
const { data: branches } = useGetIntegrationAuthVercelBranches({
|
||||||
integrationAuthId: integrationAuthId as string,
|
integrationAuthId: integrationAuthId as string,
|
||||||
appId: targetAppId
|
appId: targetAppId
|
||||||
@@ -135,6 +141,26 @@ export const VercelConfigurePage = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const selectedVercelEnvironments = useMemo(() => {
|
||||||
|
let selectedEnvironments = vercelEnvironments;
|
||||||
|
|
||||||
|
const environments = customEnvironments?.find(
|
||||||
|
(e) => e.appId === targetAppId
|
||||||
|
)?.customEnvironments;
|
||||||
|
|
||||||
|
if (environments && environments.length > 0) {
|
||||||
|
selectedEnvironments = [
|
||||||
|
...selectedEnvironments,
|
||||||
|
...environments.map((env) => ({
|
||||||
|
name: env.slug,
|
||||||
|
slug: env.id
|
||||||
|
}))
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedEnvironments;
|
||||||
|
}, [targetAppId, customEnvironments]);
|
||||||
|
|
||||||
return integrationAuth &&
|
return integrationAuth &&
|
||||||
selectedSourceEnvironment &&
|
selectedSourceEnvironment &&
|
||||||
integrationAuthApps &&
|
integrationAuthApps &&
|
||||||
@@ -210,7 +236,13 @@ export const VercelConfigurePage = () => {
|
|||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
value={targetAppId}
|
value={targetAppId}
|
||||||
onValueChange={(val) => setTargetAppId(val)}
|
onValueChange={(val) => {
|
||||||
|
if (vercelEnvironments.every((env) => env.slug !== targetEnvironment)) {
|
||||||
|
setTargetEnvironment(vercelEnvironments[0].slug);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTargetAppId(val);
|
||||||
|
}}
|
||||||
className="w-full border border-mineshaft-500"
|
className="w-full border border-mineshaft-500"
|
||||||
isDisabled={integrationAuthApps.length === 0}
|
isDisabled={integrationAuthApps.length === 0}
|
||||||
>
|
>
|
||||||
@@ -236,7 +268,7 @@ export const VercelConfigurePage = () => {
|
|||||||
onValueChange={(val) => setTargetEnvironment(val)}
|
onValueChange={(val) => setTargetEnvironment(val)}
|
||||||
className="w-full border border-mineshaft-500"
|
className="w-full border border-mineshaft-500"
|
||||||
>
|
>
|
||||||
{vercelEnvironments.map((vercelEnvironment) => (
|
{selectedVercelEnvironments.map((vercelEnvironment) => (
|
||||||
<SelectItem
|
<SelectItem
|
||||||
value={vercelEnvironment.slug}
|
value={vercelEnvironment.slug}
|
||||||
key={`target-environment-${vercelEnvironment.slug}`}
|
key={`target-environment-${vercelEnvironment.slug}`}
|
||||||
|
|||||||
Reference in New Issue
Block a user