mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 21:28:18 +00:00
added support for manual syncing of integrations
This commit is contained in:
@@ -51,6 +51,7 @@ export enum EventType {
|
|||||||
UNAUTHORIZE_INTEGRATION = "unauthorize-integration",
|
UNAUTHORIZE_INTEGRATION = "unauthorize-integration",
|
||||||
CREATE_INTEGRATION = "create-integration",
|
CREATE_INTEGRATION = "create-integration",
|
||||||
DELETE_INTEGRATION = "delete-integration",
|
DELETE_INTEGRATION = "delete-integration",
|
||||||
|
MANUAL_SYNC_INTEGRATION = "manual-sync-integration",
|
||||||
ADD_TRUSTED_IP = "add-trusted-ip",
|
ADD_TRUSTED_IP = "add-trusted-ip",
|
||||||
UPDATE_TRUSTED_IP = "update-trusted-ip",
|
UPDATE_TRUSTED_IP = "update-trusted-ip",
|
||||||
DELETE_TRUSTED_IP = "delete-trusted-ip",
|
DELETE_TRUSTED_IP = "delete-trusted-ip",
|
||||||
@@ -281,6 +282,25 @@ interface DeleteIntegrationEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ManualSyncIntegrationEvent {
|
||||||
|
type: EventType.MANUAL_SYNC_INTEGRATION;
|
||||||
|
metadata: {
|
||||||
|
integrationId: string;
|
||||||
|
integration: string;
|
||||||
|
environment: string;
|
||||||
|
secretPath: string;
|
||||||
|
url?: string;
|
||||||
|
app?: string;
|
||||||
|
appId?: string;
|
||||||
|
targetEnvironment?: string;
|
||||||
|
targetEnvironmentId?: string;
|
||||||
|
targetService?: string;
|
||||||
|
targetServiceId?: string;
|
||||||
|
path?: string;
|
||||||
|
region?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface AddTrustedIPEvent {
|
interface AddTrustedIPEvent {
|
||||||
type: EventType.ADD_TRUSTED_IP;
|
type: EventType.ADD_TRUSTED_IP;
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -791,6 +811,7 @@ export type Event =
|
|||||||
| UnauthorizeIntegrationEvent
|
| UnauthorizeIntegrationEvent
|
||||||
| CreateIntegrationEvent
|
| CreateIntegrationEvent
|
||||||
| DeleteIntegrationEvent
|
| DeleteIntegrationEvent
|
||||||
|
| ManualSyncIntegrationEvent
|
||||||
| AddTrustedIPEvent
|
| AddTrustedIPEvent
|
||||||
| UpdateTrustedIPEvent
|
| UpdateTrustedIPEvent
|
||||||
| DeleteTrustedIPEvent
|
| DeleteTrustedIPEvent
|
||||||
|
|||||||
@@ -649,6 +649,9 @@ export const INTEGRATION = {
|
|||||||
},
|
},
|
||||||
DELETE: {
|
DELETE: {
|
||||||
integrationId: "The ID of the integration object."
|
integrationId: "The ID of the integration object."
|
||||||
|
},
|
||||||
|
SYNC: {
|
||||||
|
integrationId: "The ID of the integration object to manually sync"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -262,5 +262,64 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO(akhilmhdh-pg): manual sync
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/:integrationId/sync",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "Manually trigger sync of an integration by integration id",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
params: z.object({
|
||||||
|
integrationId: z.string().trim().describe(INTEGRATION.SYNC.integrationId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
integration: IntegrationsSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const integration = await server.services.integration.syncIntegration({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
id: req.params.integrationId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: integration.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.MANUAL_SYNC_INTEGRATION,
|
||||||
|
// eslint-disable-next-line
|
||||||
|
metadata: shake({
|
||||||
|
integrationId: integration.id,
|
||||||
|
integration: integration.integration,
|
||||||
|
environment: integration.environment.slug,
|
||||||
|
secretPath: integration.secretPath,
|
||||||
|
url: integration.url,
|
||||||
|
app: integration.app,
|
||||||
|
appId: integration.appId,
|
||||||
|
targetEnvironment: integration.targetEnvironment,
|
||||||
|
targetEnvironmentId: integration.targetEnvironmentId,
|
||||||
|
targetService: integration.targetService,
|
||||||
|
targetServiceId: integration.targetServiceId,
|
||||||
|
path: integration.path,
|
||||||
|
region: integration.region
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}) as any
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { integration };
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,7 +9,12 @@ import { TIntegrationAuthDALFactory } from "../integration-auth/integration-auth
|
|||||||
import { TSecretQueueFactory } from "../secret/secret-queue";
|
import { TSecretQueueFactory } from "../secret/secret-queue";
|
||||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
import { TIntegrationDALFactory } from "./integration-dal";
|
import { TIntegrationDALFactory } from "./integration-dal";
|
||||||
import { TCreateIntegrationDTO, TDeleteIntegrationDTO, TUpdateIntegrationDTO } from "./integration-types";
|
import {
|
||||||
|
TCreateIntegrationDTO,
|
||||||
|
TDeleteIntegrationDTO,
|
||||||
|
TSyncIntegrationDTO,
|
||||||
|
TUpdateIntegrationDTO
|
||||||
|
} from "./integration-types";
|
||||||
|
|
||||||
type TIntegrationServiceFactoryDep = {
|
type TIntegrationServiceFactoryDep = {
|
||||||
integrationDAL: TIntegrationDALFactory;
|
integrationDAL: TIntegrationDALFactory;
|
||||||
@@ -201,10 +206,35 @@ export const integrationServiceFactory = ({
|
|||||||
return integrations;
|
return integrations;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const syncIntegration = async ({ id, actorId, actor, actorOrgId, actorAuthMethod }: TSyncIntegrationDTO) => {
|
||||||
|
const integration = await integrationDAL.findById(id);
|
||||||
|
if (!integration) {
|
||||||
|
throw new BadRequestError({ message: "Integration not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
integration.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
|
||||||
|
|
||||||
|
await secretQueueService.syncIntegrations({
|
||||||
|
environment: integration.environment.slug,
|
||||||
|
secretPath: integration.secretPath,
|
||||||
|
projectId: integration.projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
return { ...integration, envId: integration.environment.id };
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
createIntegration,
|
createIntegration,
|
||||||
updateIntegration,
|
updateIntegration,
|
||||||
deleteIntegration,
|
deleteIntegration,
|
||||||
listIntegrationByProject
|
listIntegrationByProject,
|
||||||
|
syncIntegration
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -59,3 +59,7 @@ export type TUpdateIntegrationDTO = {
|
|||||||
export type TDeleteIntegrationDTO = {
|
export type TDeleteIntegrationDTO = {
|
||||||
id: string;
|
id: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TSyncIntegrationDTO = {
|
||||||
|
id: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
import { MutableRefObject } from "react";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
import { workspaceKeys } from "../workspace/queries";
|
import { workspaceKeys } from "../workspace/queries";
|
||||||
import { TCloudIntegration } from "./types";
|
import { TCloudIntegration, TIntegration } from "./types";
|
||||||
|
|
||||||
export const integrationQueryKeys = {
|
export const integrationQueryKeys = {
|
||||||
getIntegrations: () => ["integrations"] as const
|
getIntegrations: () => ["integrations"] as const
|
||||||
@@ -110,3 +112,32 @@ export const useDeleteIntegration = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useSyncIntegration = (pollingRef: MutableRefObject<NodeJS.Timeout | null>) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<{}, {}, { id: string; workspaceId: string; lastUsed: string }>({
|
||||||
|
mutationFn: ({ id }) => apiRequest.post(`/api/v1/integration/${id}/sync`),
|
||||||
|
onSuccess: (_, { id, workspaceId, lastUsed }) => {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
pollingRef.current = setInterval(() => {
|
||||||
|
const integrations: TIntegration[] | undefined = queryClient.getQueryData(
|
||||||
|
workspaceKeys.getWorkspaceIntegrations(workspaceId)
|
||||||
|
);
|
||||||
|
|
||||||
|
const integration = integrations?.find((entry) => entry.id === id);
|
||||||
|
if (!integration || integration.lastUsed !== lastUsed) {
|
||||||
|
createNotification({
|
||||||
|
text: "Integration successfully synced",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
clearInterval(pollingRef.current as NodeJS.Timeout);
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
pollingRef.current = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
queryClient.invalidateQueries(workspaceKeys.getWorkspaceIntegrations(workspaceId));
|
||||||
|
}, 3500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
+42
-2
@@ -1,6 +1,7 @@
|
|||||||
|
import { useEffect, useRef } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { faCalendarCheck } from "@fortawesome/free-regular-svg-icons";
|
import { faCalendarCheck } from "@fortawesome/free-regular-svg-icons";
|
||||||
import { faArrowRight, faXmark } from "@fortawesome/free-solid-svg-icons";
|
import { faArrowRight, faRefresh, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { integrationSlugNameMapping } from "public/data/frequentConstants";
|
import { integrationSlugNameMapping } from "public/data/frequentConstants";
|
||||||
@@ -9,6 +10,7 @@ import { ProjectPermissionCan } from "@app/components/permissions";
|
|||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
AlertDescription,
|
AlertDescription,
|
||||||
|
Button,
|
||||||
DeleteActionModal,
|
DeleteActionModal,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
@@ -19,6 +21,7 @@ import {
|
|||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
|
import { useSyncIntegration } from "@app/hooks/api/integrations/queries";
|
||||||
import { TIntegration } from "@app/hooks/api/types";
|
import { TIntegration } from "@app/hooks/api/types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -42,6 +45,17 @@ export const IntegrationsSection = ({
|
|||||||
"deleteConfirmation"
|
"deleteConfirmation"
|
||||||
] as const);
|
] as const);
|
||||||
|
|
||||||
|
const syncPollingRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const { mutate: syncIntegration } = useSyncIntegration(syncPollingRef);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (syncPollingRef.current) {
|
||||||
|
clearInterval(syncPollingRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<div className="mx-4 mb-4 mt-6 flex flex-col items-start justify-between px-2 text-xl">
|
<div className="mx-4 mb-4 mt-6 flex flex-col items-start justify-between px-2 text-xl">
|
||||||
@@ -193,6 +207,7 @@ export const IntegrationsSection = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="mt-[1.5rem] flex cursor-default">
|
<div className="mt-[1.5rem] flex cursor-default">
|
||||||
{integration.isSynced != null && integration.lastUsed != null && (
|
{integration.isSynced != null && integration.lastUsed != null && (
|
||||||
|
<>
|
||||||
<Tag
|
<Tag
|
||||||
key={integration.id}
|
key={integration.id}
|
||||||
className={integration.isSynced ? "bg-green/50" : "bg-red/80"}
|
className={integration.isSynced ? "bg-green/50" : "bg-red/80"}
|
||||||
@@ -229,13 +244,38 @@ export const IntegrationsSection = ({
|
|||||||
<div className="px-2 text-white">Sync Status</div>
|
<div className="px-2 text-white">Sync Status</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Tag>
|
</Tag>
|
||||||
|
{!integration.isSynced && integration.lastUsed != null && (
|
||||||
|
<div className="mr-1 flex items-end opacity-80 duration-200 hover:opacity-100">
|
||||||
|
<Tooltip
|
||||||
|
className="text-center"
|
||||||
|
content="Manually sync integration secrets"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
onClick={() =>
|
||||||
|
syncIntegration({
|
||||||
|
workspaceId,
|
||||||
|
id: integration.id,
|
||||||
|
lastUsed: integration.lastUsed as string
|
||||||
|
})
|
||||||
|
}
|
||||||
|
isLoading={!!syncPollingRef.current}
|
||||||
|
className="max-w-[2.5rem]"
|
||||||
|
isDisabled={!!syncPollingRef.current}
|
||||||
|
colorSchema="primary"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faRefresh} className="px-1" />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Delete}
|
I={ProjectPermissionActions.Delete}
|
||||||
a={ProjectPermissionSub.Integrations}
|
a={ProjectPermissionSub.Integrations}
|
||||||
>
|
>
|
||||||
{(isAllowed: boolean) => (
|
{(isAllowed: boolean) => (
|
||||||
<div className="ml-2 flex items-end opacity-80 duration-200 hover:opacity-100">
|
<div className="flex items-end opacity-80 duration-200 hover:opacity-100">
|
||||||
<Tooltip content="Remove Integration">
|
<Tooltip content="Remove Integration">
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => handlePopUpOpen("deleteConfirmation", integration)}
|
onClick={() => handlePopUpOpen("deleteConfirmation", integration)}
|
||||||
|
|||||||
Reference in New Issue
Block a user