mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-11 17:29:00 +00:00
feat: Support Checkly group variables (ENG-3478) (#4418)
* feat: checkly group sync * fix: remove scope discriminator * fix: forms * fix: queries * fix: 500 error * fix: update docs * lint: fix * fix: review changes * fix: PR changes * fix: resolve group select UI not clearing --------- Co-authored-by: Scott Wilson <[email protected]>
This commit is contained in:
@@ -53,4 +53,36 @@ export const registerChecklyConnectionRouter = async (server: FastifyZodProvider
|
||||
return { accounts };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/:connectionId/accounts/:accountId/groups`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
connectionId: z.string().uuid(),
|
||||
accountId: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
groups: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
id: z.string()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { connectionId, accountId } = req.params;
|
||||
|
||||
const groups = await server.services.appConnection.checkly.listGroups(connectionId, accountId, req.permission);
|
||||
|
||||
return { groups };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import { AxiosInstance, AxiosRequestConfig, AxiosResponse, HttpStatusCode, isAxi
|
||||
|
||||
import { createRequestClient } from "@app/lib/config/request";
|
||||
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
|
||||
|
||||
import { ChecklyConnectionMethod } from "./checkly-connection-constants";
|
||||
import { TChecklyAccount, TChecklyConnectionConfig, TChecklyVariable } from "./checkly-connection-types";
|
||||
@@ -181,6 +182,122 @@ class ChecklyPublicClient {
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
async getCheckGroups(connection: TChecklyConnectionConfig, accountId: string, limit = 50, page = 1) {
|
||||
const res = await this.send<{ id: number; name: string }[]>(connection, {
|
||||
accountId,
|
||||
method: "GET",
|
||||
url: `/v1/check-groups`,
|
||||
params: { limit, page }
|
||||
});
|
||||
|
||||
return res?.map((group) => ({
|
||||
id: group.id.toString(),
|
||||
name: group.name
|
||||
}));
|
||||
}
|
||||
|
||||
async getCheckGroup(connection: TChecklyConnectionConfig, accountId: string, groupId: string) {
|
||||
try {
|
||||
type ChecklyGroupResponse = {
|
||||
id: number;
|
||||
name: string;
|
||||
environmentVariables: Array<{
|
||||
key: string;
|
||||
value: string;
|
||||
locked: boolean;
|
||||
}>;
|
||||
};
|
||||
|
||||
const res = await this.send<ChecklyGroupResponse>(connection, {
|
||||
accountId,
|
||||
method: "GET",
|
||||
url: `/v1/check-groups/${groupId}`
|
||||
});
|
||||
|
||||
if (!res) return null;
|
||||
|
||||
return {
|
||||
id: res.id.toString(),
|
||||
name: res.name,
|
||||
environmentVariables: res.environmentVariables
|
||||
};
|
||||
} catch (error) {
|
||||
if (isAxiosError(error) && error.response?.status === HttpStatusCode.NotFound) {
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async updateCheckGroupEnvironmentVariables(
|
||||
connection: TChecklyConnectionConfig,
|
||||
accountId: string,
|
||||
groupId: string,
|
||||
environmentVariables: Array<{ key: string; value: string; locked?: boolean }>
|
||||
) {
|
||||
if (environmentVariables.length > 50) {
|
||||
throw new SecretSyncError({
|
||||
message: "Checkly does not support syncing more than 50 variables to Check Group",
|
||||
shouldRetry: false
|
||||
});
|
||||
}
|
||||
|
||||
const apiVariables = environmentVariables.map((v) => ({
|
||||
key: v.key,
|
||||
value: v.value,
|
||||
locked: v.locked ?? false,
|
||||
secret: true
|
||||
}));
|
||||
|
||||
const group = await this.getCheckGroup(connection, accountId, groupId);
|
||||
|
||||
await this.send(connection, {
|
||||
accountId,
|
||||
method: "PUT",
|
||||
url: `/v2/check-groups/${groupId}`,
|
||||
data: { name: group?.name, environmentVariables: apiVariables }
|
||||
});
|
||||
|
||||
return this.getCheckGroup(connection, accountId, groupId);
|
||||
}
|
||||
|
||||
async getCheckGroupEnvironmentVariables(connection: TChecklyConnectionConfig, accountId: string, groupId: string) {
|
||||
const group = await this.getCheckGroup(connection, accountId, groupId);
|
||||
return group?.environmentVariables || [];
|
||||
}
|
||||
|
||||
async upsertCheckGroupEnvironmentVariables(
|
||||
connection: TChecklyConnectionConfig,
|
||||
accountId: string,
|
||||
groupId: string,
|
||||
variables: Array<{ key: string; value: string; locked?: boolean }>
|
||||
) {
|
||||
const existingVars = await this.getCheckGroupEnvironmentVariables(connection, accountId, groupId);
|
||||
const varMap = new Map(existingVars.map((v) => [v.key, v]));
|
||||
|
||||
for (const newVar of variables) {
|
||||
varMap.set(newVar.key, {
|
||||
key: newVar.key,
|
||||
value: newVar.value,
|
||||
locked: newVar.locked ?? false
|
||||
});
|
||||
}
|
||||
|
||||
return this.updateCheckGroupEnvironmentVariables(connection, accountId, groupId, Array.from(varMap.values()));
|
||||
}
|
||||
|
||||
async deleteCheckGroupEnvironmentVariable(
|
||||
connection: TChecklyConnectionConfig,
|
||||
accountId: string,
|
||||
groupId: string,
|
||||
variableKey: string
|
||||
) {
|
||||
const existingVars = await this.getCheckGroupEnvironmentVariables(connection, accountId, groupId);
|
||||
const filteredVars = existingVars.filter((v) => v.key !== variableKey);
|
||||
|
||||
return this.updateCheckGroupEnvironmentVariables(connection, accountId, groupId, filteredVars);
|
||||
}
|
||||
}
|
||||
|
||||
export const ChecklyPublicAPI = new ChecklyPublicClient();
|
||||
|
||||
@@ -24,7 +24,19 @@ export const checklyConnectionService = (getAppConnection: TGetAppConnectionFunc
|
||||
}
|
||||
};
|
||||
|
||||
const listGroups = async (connectionId: string, accountId: string, actor: OrgServiceActor) => {
|
||||
const appConnection = await getAppConnection(AppConnection.Checkly, connectionId, actor);
|
||||
try {
|
||||
const groups = await ChecklyPublicAPI.getCheckGroups(appConnection, accountId);
|
||||
return groups!;
|
||||
} catch (error) {
|
||||
logger.error(error, "Failed to list accounts on Checkly");
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
listAccounts
|
||||
listAccounts,
|
||||
listGroups
|
||||
};
|
||||
};
|
||||
|
||||
@@ -33,3 +33,15 @@ export type TChecklyAccount = {
|
||||
name: string;
|
||||
runtimeId: string;
|
||||
};
|
||||
|
||||
export type TChecklyGroupEnvironmentVariable = {
|
||||
key: string;
|
||||
value: string;
|
||||
locked: boolean;
|
||||
};
|
||||
|
||||
export type TChecklyGroup = {
|
||||
id: string;
|
||||
name: string;
|
||||
environmentVariables?: TChecklyGroupEnvironmentVariable[];
|
||||
};
|
||||
|
||||
@@ -23,56 +23,120 @@ export const ChecklySyncFns = {
|
||||
|
||||
const config = secretSync.destinationConfig;
|
||||
|
||||
const variables = await ChecklyPublicAPI.getVariables(secretSync.connection, config.accountId);
|
||||
if (config.groupId) {
|
||||
// Handle group environment variables
|
||||
const groupVars = await ChecklyPublicAPI.getCheckGroupEnvironmentVariables(
|
||||
secretSync.connection,
|
||||
config.accountId,
|
||||
config.groupId
|
||||
);
|
||||
|
||||
const checklySecrets = Object.fromEntries(variables!.map((variable) => [variable.key, variable]));
|
||||
const checklyGroupSecrets = Object.fromEntries(groupVars.map((variable) => [variable.key, variable]));
|
||||
|
||||
for await (const key of Object.keys(secretMap)) {
|
||||
try {
|
||||
// Prepare all variables to update at once
|
||||
const updatedVariables = { ...checklyGroupSecrets };
|
||||
|
||||
for (const key of Object.keys(secretMap)) {
|
||||
const entry = secretMap[key];
|
||||
|
||||
// If value is empty, we skip the upsert - checkly does not allow empty values
|
||||
// If value is empty, we skip adding it - checkly does not allow empty values
|
||||
if (entry.value.trim() === "") {
|
||||
// Delete the secret from Checkly if its empty
|
||||
// Delete the secret from the group if it's empty
|
||||
if (!disableSecretDeletion) {
|
||||
await ChecklyPublicAPI.deleteVariable(secretSync.connection, config.accountId, {
|
||||
key
|
||||
});
|
||||
delete updatedVariables[key];
|
||||
}
|
||||
continue; // Skip empty values
|
||||
}
|
||||
|
||||
await ChecklyPublicAPI.upsertVariable(secretSync.connection, config.accountId, {
|
||||
// Add or update the variable
|
||||
updatedVariables[key] = {
|
||||
key,
|
||||
value: entry.value,
|
||||
secret: true,
|
||||
locked: true
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// Remove secrets that are not in the secretMap if deletion is enabled
|
||||
if (!disableSecretDeletion) {
|
||||
for (const key of Object.keys(checklyGroupSecrets)) {
|
||||
// eslint-disable-next-line no-continue
|
||||
if (!matchesSchema(key, environment?.slug || "", keySchema)) continue;
|
||||
|
||||
if (!secretMap[key]) {
|
||||
delete updatedVariables[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update all group environment variables at once
|
||||
try {
|
||||
await ChecklyPublicAPI.updateCheckGroupEnvironmentVariables(
|
||||
secretSync.connection,
|
||||
config.accountId,
|
||||
config.groupId,
|
||||
Object.values(updatedVariables)
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof SecretSyncError) throw error;
|
||||
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
secretKey: "group_update"
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Handle global variables (existing logic)
|
||||
const variables = await ChecklyPublicAPI.getVariables(secretSync.connection, config.accountId);
|
||||
|
||||
if (disableSecretDeletion) return;
|
||||
const checklySecrets = Object.fromEntries(variables!.map((variable) => [variable.key, variable]));
|
||||
|
||||
for await (const key of Object.keys(checklySecrets)) {
|
||||
try {
|
||||
// eslint-disable-next-line no-continue
|
||||
if (!matchesSchema(key, environment?.slug || "", keySchema)) continue;
|
||||
for await (const key of Object.keys(secretMap)) {
|
||||
try {
|
||||
const entry = secretMap[key];
|
||||
|
||||
if (!secretMap[key]) {
|
||||
await ChecklyPublicAPI.deleteVariable(secretSync.connection, config.accountId, {
|
||||
key
|
||||
// If value is empty, we skip the upsert - checkly does not allow empty values
|
||||
if (entry.value.trim() === "") {
|
||||
// Delete the secret from Checkly if its empty
|
||||
if (!disableSecretDeletion) {
|
||||
await ChecklyPublicAPI.deleteVariable(secretSync.connection, config.accountId, {
|
||||
key
|
||||
});
|
||||
}
|
||||
continue; // Skip empty values
|
||||
}
|
||||
|
||||
await ChecklyPublicAPI.upsertVariable(secretSync.connection, config.accountId, {
|
||||
key,
|
||||
value: entry.value,
|
||||
secret: true,
|
||||
locked: true
|
||||
});
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (disableSecretDeletion) return;
|
||||
|
||||
for await (const key of Object.keys(checklySecrets)) {
|
||||
try {
|
||||
// eslint-disable-next-line no-continue
|
||||
if (!matchesSchema(key, environment?.slug || "", keySchema)) continue;
|
||||
|
||||
if (!secretMap[key]) {
|
||||
await ChecklyPublicAPI.deleteVariable(secretSync.connection, config.accountId, {
|
||||
key
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -80,23 +144,54 @@ export const ChecklySyncFns = {
|
||||
async removeSecrets(secretSync: TChecklySyncWithCredentials, secretMap: TSecretMap) {
|
||||
const config = secretSync.destinationConfig;
|
||||
|
||||
const variables = await ChecklyPublicAPI.getVariables(secretSync.connection, config.accountId);
|
||||
if (config.groupId) {
|
||||
// Handle group environment variables
|
||||
const groupVars = await ChecklyPublicAPI.getCheckGroupEnvironmentVariables(
|
||||
secretSync.connection,
|
||||
config.accountId,
|
||||
config.groupId
|
||||
);
|
||||
|
||||
const checklySecrets = Object.fromEntries(variables!.map((variable) => [variable.key, variable]));
|
||||
const checklyGroupSecrets = Object.fromEntries(groupVars.map((variable) => [variable.key, variable]));
|
||||
|
||||
// Filter out the secrets to remove
|
||||
const remainingVariables = Object.keys(checklyGroupSecrets)
|
||||
.filter((key) => !(key in secretMap))
|
||||
.map((key) => checklyGroupSecrets[key]);
|
||||
|
||||
for await (const secret of Object.keys(checklySecrets)) {
|
||||
try {
|
||||
if (secret in secretMap) {
|
||||
await ChecklyPublicAPI.deleteVariable(secretSync.connection, config.accountId, {
|
||||
key: secret
|
||||
});
|
||||
}
|
||||
await ChecklyPublicAPI.updateCheckGroupEnvironmentVariables(
|
||||
secretSync.connection,
|
||||
config.accountId,
|
||||
config.groupId,
|
||||
remainingVariables
|
||||
);
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: secret
|
||||
secretKey: "group_remove"
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Handle global variables (existing logic)
|
||||
const variables = await ChecklyPublicAPI.getVariables(secretSync.connection, config.accountId);
|
||||
|
||||
const checklySecrets = Object.fromEntries(variables!.map((variable) => [variable.key, variable]));
|
||||
|
||||
for await (const secret of Object.keys(checklySecrets)) {
|
||||
try {
|
||||
if (secret in secretMap) {
|
||||
await ChecklyPublicAPI.deleteVariable(secretSync.connection, config.accountId, {
|
||||
key: secret
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: secret
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -11,7 +11,17 @@ import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"
|
||||
|
||||
const ChecklySyncDestinationConfigSchema = z.object({
|
||||
accountId: z.string().min(1, "Account ID is required").max(255, "Account ID must be less than 255 characters"),
|
||||
accountName: z.string().min(1, "Account Name is required").max(255, "Account ID must be less than 255 characters")
|
||||
accountName: z
|
||||
.string()
|
||||
.min(1, "Account Name is required")
|
||||
.max(255, "Account ID must be less than 255 characters")
|
||||
.optional(),
|
||||
groupId: z.string().min(1, "Group ID is required").max(255, "Group ID must be less than 255 characters").optional(),
|
||||
groupName: z
|
||||
.string()
|
||||
.min(1, "Group Name is required")
|
||||
.max(255, "Group Name must be less than 255 characters")
|
||||
.optional()
|
||||
});
|
||||
|
||||
const ChecklySyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false };
|
||||
|
||||
Reference in New Issue
Block a user