mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 08:27:36 +00:00
feat(relays): healthcheck
This commit is contained in:
@@ -0,0 +1,19 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasColumn(TableName.Relay, "heartbeat"))) {
|
||||||
|
await knex.schema.alterTable(TableName.Relay, (t) => {
|
||||||
|
t.datetime("heartbeat");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasColumn(TableName.Relay, "heartbeat")) {
|
||||||
|
await knex.schema.alterTable(TableName.Relay, (t) => {
|
||||||
|
t.dropColumn("heartbeat");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,8 @@ export const RelaysSchema = z.object({
|
|||||||
orgId: z.string().uuid().nullable().optional(),
|
orgId: z.string().uuid().nullable().optional(),
|
||||||
identityId: z.string().uuid().nullable().optional(),
|
identityId: z.string().uuid().nullable().optional(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
host: z.string()
|
host: z.string(),
|
||||||
|
heartbeat: z.date().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TRelays = z.infer<typeof RelaysSchema>;
|
export type TRelays = z.infer<typeof RelaysSchema>;
|
||||||
|
|||||||
@@ -146,4 +146,75 @@ export const registerRelayRouter = async (server: FastifyZodProvider) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/heartbeat-instance-relay",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
name: slugSchema({ min: 1, max: 32, field: "name" })
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
message: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: (req, _, next) => {
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
|
||||||
|
if (appCfg.RELAY_AUTH_SECRET && authHeader) {
|
||||||
|
const expectedHeader = `Bearer ${appCfg.RELAY_AUTH_SECRET}`;
|
||||||
|
if (
|
||||||
|
authHeader.length === expectedHeader.length &&
|
||||||
|
crypto.nativeCrypto.timingSafeEqual(Buffer.from(authHeader), Buffer.from(expectedHeader))
|
||||||
|
) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new UnauthorizedError({
|
||||||
|
message: "Invalid relay auth secret"
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
await server.services.relay.heartbeat({
|
||||||
|
name: req.body.name
|
||||||
|
});
|
||||||
|
|
||||||
|
return { message: "Successfully triggered heartbeat" };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/heartbeat-org-relay",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
name: slugSchema({ min: 1, max: 32, field: "name" })
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
message: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
await server.services.relay.heartbeat({
|
||||||
|
name: req.body.name,
|
||||||
|
identityId: req.permission.id,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
actorAuthMethod: req.permission.authMethod
|
||||||
|
});
|
||||||
|
|
||||||
|
return { message: "Successfully triggered heartbeat" };
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import * as x509 from "@peculiar/x509";
|
|||||||
import { TRelays } from "@app/db/schemas";
|
import { TRelays } from "@app/db/schemas";
|
||||||
import { PgSqlLock } from "@app/keystore/keystore";
|
import { PgSqlLock } from "@app/keystore/keystore";
|
||||||
import { crypto } from "@app/lib/crypto";
|
import { crypto } from "@app/lib/crypto";
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { createRelayConnection } from "@app/lib/gateway-v2/gateway-v2";
|
||||||
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
||||||
import { constructPemChainFromCerts, prependCertToPemChain } from "@app/services/certificate/certificate-fns";
|
import { constructPemChainFromCerts, prependCertToPemChain } from "@app/services/certificate/certificate-fns";
|
||||||
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
|
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
|
||||||
@@ -1056,6 +1057,78 @@ export const relayServiceFactory = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const heartbeat = async ({
|
||||||
|
name,
|
||||||
|
identityId,
|
||||||
|
actorAuthMethod,
|
||||||
|
orgId
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
identityId?: string;
|
||||||
|
actorAuthMethod?: ActorAuthMethod;
|
||||||
|
orgId?: string;
|
||||||
|
}) => {
|
||||||
|
const relay = await relayDAL.findOne({
|
||||||
|
name,
|
||||||
|
orgId: orgId ?? null
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!relay) {
|
||||||
|
throw new NotFoundError({ message: `Relay with name ${name} not found.` });
|
||||||
|
}
|
||||||
|
|
||||||
|
let clientOrgId: string;
|
||||||
|
let clientOrgName: string;
|
||||||
|
|
||||||
|
if (relay.orgId) {
|
||||||
|
if (!identityId || !orgId || relay.orgId !== orgId) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "You do not have permission to perform this action on this relay."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
ActorType.IDENTITY,
|
||||||
|
identityId,
|
||||||
|
orgId,
|
||||||
|
actorAuthMethod!,
|
||||||
|
orgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionRelayActions.CreateRelays,
|
||||||
|
OrgPermissionSubjects.Relay
|
||||||
|
);
|
||||||
|
clientOrgId = orgId;
|
||||||
|
clientOrgName = orgId;
|
||||||
|
} else {
|
||||||
|
clientOrgId = "00000000-0000-0000-0000-000000000000";
|
||||||
|
clientOrgName = "heartbeat";
|
||||||
|
}
|
||||||
|
|
||||||
|
const relayClientCredentials = await getCredentialsForClient({
|
||||||
|
relayId: relay.id,
|
||||||
|
orgId: clientOrgId,
|
||||||
|
orgName: clientOrgName,
|
||||||
|
gatewayId: "00000000-0000-0000-0000-000000000000",
|
||||||
|
gatewayName: "heartbeat",
|
||||||
|
duration: 60 * 1000 // 1 minute
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await createRelayConnection({
|
||||||
|
relayHost: relayClientCredentials.relayHost,
|
||||||
|
clientCertificate: relayClientCredentials.clientCertificate,
|
||||||
|
clientPrivateKey: relayClientCredentials.clientPrivateKey,
|
||||||
|
serverCertificateChain: relayClientCredentials.serverCertificateChain
|
||||||
|
});
|
||||||
|
|
||||||
|
await relayDAL.updateById(relay.id, { heartbeat: new Date() });
|
||||||
|
} catch (err) {
|
||||||
|
const error = err as Error;
|
||||||
|
throw new BadRequestError({ message: `Relay ${name} is not reachable: ${error.message}` });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getRelays = async ({
|
const getRelays = async ({
|
||||||
actorId,
|
actorId,
|
||||||
actor,
|
actor,
|
||||||
@@ -1125,6 +1198,7 @@ export const relayServiceFactory = ({
|
|||||||
getCredentialsForGateway,
|
getCredentialsForGateway,
|
||||||
getCredentialsForClient,
|
getCredentialsForClient,
|
||||||
getRelays,
|
getRelays,
|
||||||
deleteRelay
|
deleteRelay,
|
||||||
|
heartbeat
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ interface IGatewayRelayServer {
|
|||||||
getRelayError: () => string;
|
getRelayError: () => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const createRelayConnection = async ({
|
export const createRelayConnection = async ({
|
||||||
relayHost,
|
relayHost,
|
||||||
clientCertificate,
|
clientCertificate,
|
||||||
clientPrivateKey,
|
clientPrivateKey,
|
||||||
|
|||||||
+4
-1
@@ -199,7 +199,10 @@ export const SecretSyncReviewFields = () => {
|
|||||||
</p>
|
</p>
|
||||||
{duplicateProjectId && (
|
{duplicateProjectId && (
|
||||||
<p className="mt-1 text-xs text-yellow-200">
|
<p className="mt-1 text-xs text-yellow-200">
|
||||||
Duplicate found in project ID: <code className="rounded bg-yellow-800/50 px-1 py-0.5">{duplicateProjectId}</code>
|
Duplicate found in project ID:{" "}
|
||||||
|
<code className="rounded bg-yellow-800/50 px-1 py-0.5">
|
||||||
|
{duplicateProjectId}
|
||||||
|
</code>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export type TRelay = {
|
|||||||
identityId: string | null;
|
identityId: string | null;
|
||||||
name: string;
|
name: string;
|
||||||
host: string;
|
host: string;
|
||||||
|
heartbeat: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TDeleteRelayDTO = {
|
export type TDeleteRelayDTO = {
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ import { useDeleteGatewayV2ById } from "@app/hooks/api/gateways-v2";
|
|||||||
|
|
||||||
import { EditGatewayDetailsModal } from "./components/EditGatewayDetailsModal";
|
import { EditGatewayDetailsModal } from "./components/EditGatewayDetailsModal";
|
||||||
|
|
||||||
const GatewayHealthStatus = ({ heartbeat }: { heartbeat?: string }) => {
|
export const GatewayHealthStatus = ({ heartbeat }: { heartbeat?: string }) => {
|
||||||
const heartbeatDate = heartbeat ? new Date(heartbeat) : null;
|
const heartbeatDate = heartbeat ? new Date(heartbeat) : null;
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000);
|
const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
faCopy,
|
faCopy,
|
||||||
faDoorClosed,
|
faDoorClosed,
|
||||||
faEllipsisV,
|
faEllipsisV,
|
||||||
|
faInfoCircle,
|
||||||
faMagnifyingGlass,
|
faMagnifyingGlass,
|
||||||
faSearch,
|
faSearch,
|
||||||
faTrash
|
faTrash
|
||||||
@@ -41,6 +42,8 @@ import { withPermission } from "@app/hoc";
|
|||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import { useDeleteRelayById, useGetRelays } from "@app/hooks/api/relays";
|
import { useDeleteRelayById, useGetRelays } from "@app/hooks/api/relays";
|
||||||
|
|
||||||
|
import { GatewayHealthStatus } from "../GatewayTab/GatewayTab";
|
||||||
|
|
||||||
export const RelayTab = withPermission(
|
export const RelayTab = withPermission(
|
||||||
() => {
|
() => {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
@@ -106,6 +109,16 @@ export const RelayTab = withPermission(
|
|||||||
<Th className="w-1/3">Name</Th>
|
<Th className="w-1/3">Name</Th>
|
||||||
<Th>Host</Th>
|
<Th>Host</Th>
|
||||||
<Th>Created</Th>
|
<Th>Created</Th>
|
||||||
|
<Th>
|
||||||
|
Health Check
|
||||||
|
<Tooltip
|
||||||
|
asChild={false}
|
||||||
|
className="normal-case"
|
||||||
|
content="The last known healthcheck. Triggers every 1 hour."
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faInfoCircle} className="ml-2" />
|
||||||
|
</Tooltip>
|
||||||
|
</Th>
|
||||||
<Th className="w-5" />
|
<Th className="w-5" />
|
||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
@@ -129,6 +142,9 @@ export const RelayTab = withPermission(
|
|||||||
</Td>
|
</Td>
|
||||||
<Td>{el.host}</Td>
|
<Td>{el.host}</Td>
|
||||||
<Td>{formatRelative(new Date(el.createdAt), new Date())}</Td>
|
<Td>{formatRelative(new Date(el.createdAt), new Date())}</Td>
|
||||||
|
<Td>
|
||||||
|
<GatewayHealthStatus heartbeat={el.heartbeat} />
|
||||||
|
</Td>
|
||||||
<Td className="w-5">
|
<Td className="w-5">
|
||||||
<Tooltip className="max-w-sm text-center" content="Options">
|
<Tooltip className="max-w-sm text-center" content="Options">
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
|
|||||||
Reference in New Issue
Block a user