mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 11:38:55 +00:00
feature: windmill connection and sync
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
export * from "./windmill-sync-constants";
|
||||
export * from "./windmill-sync-fns";
|
||||
export * from "./windmill-sync-schemas";
|
||||
export * from "./windmill-sync-types";
|
||||
@@ -0,0 +1,10 @@
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
export const WINDMILL_SYNC_LIST_OPTION: TSecretSyncListItem = {
|
||||
name: "Windmill",
|
||||
destination: SecretSync.Windmill,
|
||||
connection: AppConnection.Windmill,
|
||||
canImportSecrets: true
|
||||
};
|
||||
@@ -0,0 +1,232 @@
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { getWindmillInstanceUrl } from "@app/services/app-connection/windmill";
|
||||
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
|
||||
import {
|
||||
TDeleteWindmillVariable,
|
||||
TPostWindmillVariable,
|
||||
TWindmillListVariables,
|
||||
TWindmillListVariablesResponse,
|
||||
TWindmillSyncWithCredentials,
|
||||
TWindmillVariable
|
||||
} from "@app/services/secret-sync/windmill/windmill-sync-types";
|
||||
|
||||
import { TSecretMap } from "../secret-sync-types";
|
||||
|
||||
const PAGE_LIMIT = 100;
|
||||
|
||||
const listWindmillVariables = async ({ instanceUrl, workspace, accessToken, path }: TWindmillListVariables) => {
|
||||
const variables: Record<string, TWindmillVariable> = {};
|
||||
|
||||
// windmill paginates but doesn't return if there's more pages so we need to check if page size full
|
||||
let page: number | null = 1;
|
||||
|
||||
while (page) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const { data: variablesPage } = await request.get<TWindmillListVariablesResponse>(
|
||||
`${instanceUrl}/api/w/${workspace}/variables/list`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
},
|
||||
params: {
|
||||
page,
|
||||
limit: PAGE_LIMIT,
|
||||
path_start: path
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
for await (const variable of variablesPage) {
|
||||
const variableName = variable.path.replace(path, "");
|
||||
|
||||
if (variable.is_secret) {
|
||||
const { data: variableValue } = await request.get<string>(
|
||||
`${instanceUrl}/api/w/${workspace}/variables/get_value/${variable.path}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
variables[variableName] = {
|
||||
...variable,
|
||||
value: variableValue
|
||||
};
|
||||
} else {
|
||||
variables[variableName] = variable;
|
||||
}
|
||||
}
|
||||
|
||||
if (variablesPage.length >= PAGE_LIMIT) {
|
||||
page += 1;
|
||||
} else {
|
||||
page = null;
|
||||
}
|
||||
}
|
||||
|
||||
return variables;
|
||||
};
|
||||
|
||||
const createWindmillVariable = async ({
|
||||
path,
|
||||
value,
|
||||
instanceUrl,
|
||||
accessToken,
|
||||
workspace,
|
||||
description
|
||||
}: TPostWindmillVariable) =>
|
||||
request.post(
|
||||
`${instanceUrl}/api/w/${workspace}/variables/create`,
|
||||
{
|
||||
path,
|
||||
value,
|
||||
is_secret: true,
|
||||
description
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const updateWindmillVariable = async ({
|
||||
path,
|
||||
value,
|
||||
instanceUrl,
|
||||
accessToken,
|
||||
workspace,
|
||||
description
|
||||
}: TPostWindmillVariable) =>
|
||||
request.post(
|
||||
`${instanceUrl}/api/w/${workspace}/variables/update/${path}`,
|
||||
{
|
||||
value,
|
||||
is_secret: true,
|
||||
description
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const deleteWindmillVariable = async ({ path, instanceUrl, accessToken, workspace }: TDeleteWindmillVariable) =>
|
||||
request.delete(`${instanceUrl}/api/w/${workspace}/variables/delete/${path}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
});
|
||||
|
||||
export const WindmillSyncFns = {
|
||||
syncSecrets: async (secretSync: TWindmillSyncWithCredentials, secretMap: TSecretMap) => {
|
||||
const {
|
||||
connection,
|
||||
destinationConfig: { workspace, path },
|
||||
syncOptions: { disableSecretDeletion }
|
||||
} = secretSync;
|
||||
|
||||
const instanceUrl = await getWindmillInstanceUrl(connection);
|
||||
|
||||
const { accessToken } = connection.credentials;
|
||||
|
||||
const variables = await listWindmillVariables({ instanceUrl, accessToken, workspace, path });
|
||||
|
||||
for await (const entry of Object.entries(secretMap)) {
|
||||
const [key, { value, comment = "" }] = entry;
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
instanceUrl,
|
||||
workspace,
|
||||
path: path + key,
|
||||
value,
|
||||
accessToken,
|
||||
description: comment
|
||||
};
|
||||
if (key in variables) {
|
||||
if (variables[key].value !== value || variables[key].description !== comment)
|
||||
await updateWindmillVariable(payload);
|
||||
} else {
|
||||
await createWindmillVariable(payload);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (disableSecretDeletion) return;
|
||||
|
||||
for await (const [key, variable] of Object.entries(variables)) {
|
||||
if (!(key in secretMap)) {
|
||||
try {
|
||||
await deleteWindmillVariable({
|
||||
instanceUrl,
|
||||
workspace,
|
||||
path: variable.path,
|
||||
accessToken
|
||||
});
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
removeSecrets: async (secretSync: TWindmillSyncWithCredentials, secretMap: TSecretMap) => {
|
||||
const {
|
||||
connection,
|
||||
destinationConfig: { workspace, path }
|
||||
} = secretSync;
|
||||
|
||||
const instanceUrl = await getWindmillInstanceUrl(connection);
|
||||
|
||||
const { accessToken } = connection.credentials;
|
||||
|
||||
const variables = await listWindmillVariables({ instanceUrl, accessToken, workspace, path });
|
||||
|
||||
for await (const [key, variable] of Object.entries(variables)) {
|
||||
if (key in secretMap) {
|
||||
try {
|
||||
await deleteWindmillVariable({
|
||||
path: variable.path,
|
||||
instanceUrl,
|
||||
workspace,
|
||||
accessToken
|
||||
});
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
getSecrets: async (secretSync: TWindmillSyncWithCredentials) => {
|
||||
const {
|
||||
connection,
|
||||
destinationConfig: { workspace, path }
|
||||
} = secretSync;
|
||||
|
||||
const instanceUrl = await getWindmillInstanceUrl(connection);
|
||||
|
||||
const { accessToken } = connection.credentials;
|
||||
|
||||
const variables = await listWindmillVariables({ instanceUrl, accessToken, workspace, path });
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(variables).map(([key, variable]) => [key, { value: variable.value ?? "" }])
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretSyncs } from "@app/lib/api-docs";
|
||||
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import {
|
||||
BaseSecretSyncSchema,
|
||||
GenericCreateSecretSyncFieldsSchema,
|
||||
GenericUpdateSecretSyncFieldsSchema
|
||||
} from "@app/services/secret-sync/secret-sync-schemas";
|
||||
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
const pathCharacterValidator = characterValidator([
|
||||
CharacterType.AlphaNumeric,
|
||||
CharacterType.Underscore,
|
||||
CharacterType.Hyphen
|
||||
]);
|
||||
|
||||
const WindmillSyncDestinationConfigSchema = z.object({
|
||||
workspace: z.string().trim().min(1, "Workspace required").describe(SecretSyncs.DESTINATION_CONFIG.WINDMILL.workspace),
|
||||
path: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Path required")
|
||||
.refine(
|
||||
(val) =>
|
||||
(val.startsWith("u/") || val.startsWith("f/")) &&
|
||||
val.endsWith("/") &&
|
||||
val.split("/").length >= 3 &&
|
||||
val
|
||||
.split("/")
|
||||
.filter(Boolean)
|
||||
.every((segment) => pathCharacterValidator(segment)),
|
||||
'Invalid path - must follow Windmill path format. ex: "/f/folder/path/"'
|
||||
)
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.WINDMILL.path)
|
||||
});
|
||||
|
||||
const WindmillSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
|
||||
|
||||
export const WindmillSyncSchema = BaseSecretSyncSchema(SecretSync.Windmill, WindmillSyncOptionsConfig).extend({
|
||||
destination: z.literal(SecretSync.Windmill),
|
||||
destinationConfig: WindmillSyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const CreateWindmillSyncSchema = GenericCreateSecretSyncFieldsSchema(
|
||||
SecretSync.Windmill,
|
||||
WindmillSyncOptionsConfig
|
||||
).extend({
|
||||
destinationConfig: WindmillSyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const UpdateWindmillSyncSchema = GenericUpdateSecretSyncFieldsSchema(
|
||||
SecretSync.Windmill,
|
||||
WindmillSyncOptionsConfig
|
||||
).extend({
|
||||
destinationConfig: WindmillSyncDestinationConfigSchema.optional()
|
||||
});
|
||||
|
||||
export const WindmillSyncListItemSchema = z.object({
|
||||
name: z.literal("Windmill"),
|
||||
connection: z.literal(AppConnection.Windmill),
|
||||
destination: z.literal(SecretSync.Windmill),
|
||||
canImportSecrets: z.literal(true)
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { TWindmillConnection } from "@app/services/app-connection/windmill";
|
||||
|
||||
import { CreateWindmillSyncSchema, WindmillSyncListItemSchema, WindmillSyncSchema } from "./windmill-sync-schemas";
|
||||
|
||||
export type TWindmillSync = z.infer<typeof WindmillSyncSchema>;
|
||||
|
||||
export type TWindmillSyncInput = z.infer<typeof CreateWindmillSyncSchema>;
|
||||
|
||||
export type TWindmillSyncListItem = z.infer<typeof WindmillSyncListItemSchema>;
|
||||
|
||||
export type TWindmillSyncWithCredentials = TWindmillSync & {
|
||||
connection: TWindmillConnection;
|
||||
};
|
||||
|
||||
export type TWindmillVariable = {
|
||||
path: string;
|
||||
value: string;
|
||||
is_secret: boolean;
|
||||
is_oauth: boolean;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export type TWindmillListVariablesResponse = TWindmillVariable[];
|
||||
|
||||
export type TWindmillListVariables = {
|
||||
accessToken: string;
|
||||
instanceUrl: string;
|
||||
path: string;
|
||||
workspace: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export type TPostWindmillVariable = TWindmillListVariables & {
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type TDeleteWindmillVariable = TWindmillListVariables;
|
||||
Reference in New Issue
Block a user