mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-12 19:29:15 +00:00
feat: adds secret sync handlers
This commit is contained in:
@@ -2512,8 +2512,11 @@ export const SecretSyncs = {
|
||||
},
|
||||
LARAVEL_FORGE: {
|
||||
orgSlug: "The slug of the Laravel Forge org to sync secrets to.",
|
||||
orgName: "The name of the Laravel Forge org to sync secrets to.",
|
||||
serverId: "The ID of the Laravel Forge server to sync secrets to.",
|
||||
siteId: "The ID of the Laravel Forge site to sync secrets to."
|
||||
serverName: "The name of the Laravel Forge server to sync secrets to.",
|
||||
siteId: "The ID of the Laravel Forge site to sync secrets to.",
|
||||
siteName: "The name of the Laravel Forge site to sync secrets to."
|
||||
},
|
||||
WINDMILL: {
|
||||
workspace: "The Windmill workspace to sync secrets to.",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./laravel-forge-sync-constants";
|
||||
export * from "./laravel-forge-sync-fns";
|
||||
export * from "./laravel-forge-sync-schemas";
|
||||
export * from "./laravel-forge-sync-types";
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
import {
|
||||
LaravelForgeSecret,
|
||||
TGetLaravelForgeSecrets,
|
||||
TLaravelForgeSecrets,
|
||||
TLaravelForgeSyncWithCredentials
|
||||
} from "./laravel-forge-sync-types";
|
||||
|
||||
const getLaravelForgeSecretsRaw = async ({ apiToken, orgSlug, serverId, siteId }: TGetLaravelForgeSecrets) => {
|
||||
const { data } = await request.get<TLaravelForgeSecrets>(
|
||||
`${IntegrationUrls.LARAVELFORGE_API_URL}/api/orgs/${orgSlug}/servers/${serverId}/sites/${siteId}/environment`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiToken}`,
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return data.data.attributes.content;
|
||||
};
|
||||
|
||||
const parseEnv = (str: string) => {
|
||||
const lines = str.split("\n");
|
||||
const parsed: { key: string; value: string }[] = [];
|
||||
|
||||
lines.forEach((line) => {
|
||||
const trimmed = line.trim();
|
||||
|
||||
const isInvalidLine = trimmed === "" || trimmed.startsWith("#");
|
||||
|
||||
if (!isInvalidLine && trimmed.includes("=")) {
|
||||
const equalIndex = trimmed.indexOf("=");
|
||||
const key = trimmed.substring(0, equalIndex).trim();
|
||||
const valueRaw = trimmed.substring(equalIndex + 1).trim();
|
||||
let value = valueRaw;
|
||||
|
||||
if ((value.startsWith(`"`) && value.endsWith(`"`)) || (value.startsWith(`'`) && value.endsWith(`'`))) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
|
||||
parsed.push({
|
||||
key,
|
||||
value
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return parsed;
|
||||
};
|
||||
|
||||
const getLaravelForgeSecrets = async (secretSync: TLaravelForgeSyncWithCredentials): Promise<LaravelForgeSecret[]> => {
|
||||
const {
|
||||
connection,
|
||||
destinationConfig: { orgSlug, serverId, siteId }
|
||||
} = secretSync;
|
||||
|
||||
const { apiToken } = connection.credentials;
|
||||
|
||||
const secrets = await getLaravelForgeSecretsRaw({ apiToken, orgSlug, serverId, siteId });
|
||||
|
||||
const parsedSecrets = parseEnv(secrets);
|
||||
|
||||
return parsedSecrets;
|
||||
};
|
||||
|
||||
const buildEnvString = (secrets: LaravelForgeSecret[]) => {
|
||||
return secrets.map((secret) => `${secret.key}=${secret.value}`).join("\n");
|
||||
};
|
||||
|
||||
const updateLaravelForgeSecrets = async (secretSync: TLaravelForgeSyncWithCredentials, envString: string) => {
|
||||
const {
|
||||
connection,
|
||||
destinationConfig: { orgSlug, serverId, siteId }
|
||||
} = secretSync;
|
||||
|
||||
const { apiToken } = connection.credentials;
|
||||
|
||||
await request.put(
|
||||
`${IntegrationUrls.LARAVELFORGE_API_URL}/api/orgs/${orgSlug}/servers/${serverId}/sites/${siteId}/environment`,
|
||||
{
|
||||
environment: envString
|
||||
},
|
||||
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiToken}`,
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const LaravelForgeSyncFns = {
|
||||
async syncSecrets(secretSync: TLaravelForgeSyncWithCredentials, secretMap: TSecretMap) {
|
||||
const secrets = await getLaravelForgeSecrets(secretSync);
|
||||
const secretsMap = Object.fromEntries(secrets.map((secret) => [secret.key, { value: secret.value }]));
|
||||
|
||||
for (const [key, { value }] of Object.entries(secretMap)) {
|
||||
secretsMap[key] = { value };
|
||||
}
|
||||
|
||||
await updateLaravelForgeSecrets(secretSync, buildEnvString(secrets));
|
||||
},
|
||||
|
||||
async getSecrets(secretSync: TLaravelForgeSyncWithCredentials): Promise<TSecretMap> {
|
||||
const secrets = await getLaravelForgeSecrets(secretSync);
|
||||
return Object.fromEntries(secrets.map((secret) => [secret.key, { value: secret.value }]));
|
||||
},
|
||||
|
||||
async removeSecrets(secretSync: TLaravelForgeSyncWithCredentials, secretMap: TSecretMap) {
|
||||
const existingSecrets = await getLaravelForgeSecrets(secretSync);
|
||||
|
||||
const newSecrets = existingSecrets.filter((secret) => !Object.hasOwn(secretMap, secret.key));
|
||||
|
||||
if (newSecrets.length === existingSecrets.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const envString = buildEnvString(newSecrets);
|
||||
|
||||
await updateLaravelForgeSecrets(secretSync, envString);
|
||||
}
|
||||
};
|
||||
@@ -12,8 +12,14 @@ import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"
|
||||
|
||||
const LaravelForgeSyncDestinationConfigSchema = z.object({
|
||||
orgSlug: z.string().min(1, "Org Slug is required").describe(SecretSyncs.DESTINATION_CONFIG.LARAVEL_FORGE.orgSlug),
|
||||
orgName: z.string().min(1, "Org Name is required").describe(SecretSyncs.DESTINATION_CONFIG.LARAVEL_FORGE.orgName),
|
||||
serverId: z.string().min(1, "Server ID is required").describe(SecretSyncs.DESTINATION_CONFIG.LARAVEL_FORGE.serverId),
|
||||
siteId: z.string().min(1, "Site ID is required").describe(SecretSyncs.DESTINATION_CONFIG.LARAVEL_FORGE.siteId)
|
||||
serverName: z
|
||||
.string()
|
||||
.min(1, "Server Name is required")
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.LARAVEL_FORGE.serverName),
|
||||
siteId: z.string().min(1, "Site ID is required").describe(SecretSyncs.DESTINATION_CONFIG.LARAVEL_FORGE.siteId),
|
||||
siteName: z.string().min(1, "Site Name is required").describe(SecretSyncs.DESTINATION_CONFIG.LARAVEL_FORGE.siteName)
|
||||
});
|
||||
|
||||
const LaravelForgeSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
|
||||
|
||||
@@ -18,24 +18,29 @@ export type TLaravelForgeSyncWithCredentials = TLaravelForgeSync & {
|
||||
connection: TLaravelForgeConnection;
|
||||
};
|
||||
|
||||
export type TGetLaravelForgeSecrets = {
|
||||
apiToken: string;
|
||||
orgSlug: string;
|
||||
serverId: string;
|
||||
siteId: string;
|
||||
};
|
||||
|
||||
export type TLaravelForgeSecrets = {
|
||||
data: {
|
||||
id: string;
|
||||
type: string;
|
||||
attributes: {
|
||||
content: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type LaravelForgeSecret = {
|
||||
description: string;
|
||||
is_secret: boolean;
|
||||
key: string;
|
||||
source: "app" | "env";
|
||||
value: string;
|
||||
};
|
||||
|
||||
export interface LaravelForgeApiSecret {
|
||||
id: string;
|
||||
key: string;
|
||||
value: string;
|
||||
type: string;
|
||||
target: string[];
|
||||
customEnvironmentIds?: string[];
|
||||
gitBranch?: string;
|
||||
createdAt?: number;
|
||||
updatedAt?: number;
|
||||
configurationId?: string;
|
||||
system?: boolean;
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ import { HEROKU_SYNC_LIST_OPTION, HerokuSyncFns } from "./heroku";
|
||||
import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec";
|
||||
import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns";
|
||||
import { LARAVEL_FORGE_SYNC_LIST_OPTION } from "./laravel-forge";
|
||||
import { LaravelForgeSyncFns } from "./laravel-forge/laravel-forge-sync-fns";
|
||||
import { NETLIFY_SYNC_LIST_OPTION, NetlifySyncFns } from "./netlify";
|
||||
import { RAILWAY_SYNC_LIST_OPTION } from "./railway/railway-sync-constants";
|
||||
import { RailwaySyncFns } from "./railway/railway-sync-fns";
|
||||
@@ -279,6 +280,8 @@ export const SecretSyncFns = {
|
||||
return NetlifySyncFns.syncSecrets(secretSync, schemaSecretMap);
|
||||
case SecretSync.Bitbucket:
|
||||
return BitbucketSyncFns.syncSecrets(secretSync, schemaSecretMap);
|
||||
case SecretSync.LaravelForge:
|
||||
return LaravelForgeSyncFns.syncSecrets(secretSync, schemaSecretMap);
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||
@@ -395,6 +398,9 @@ export const SecretSyncFns = {
|
||||
case SecretSync.Bitbucket:
|
||||
secretMap = await BitbucketSyncFns.getSecrets(secretSync);
|
||||
break;
|
||||
case SecretSync.LaravelForge:
|
||||
secretMap = await LaravelForgeSyncFns.getSecrets(secretSync);
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||
@@ -488,6 +494,8 @@ export const SecretSyncFns = {
|
||||
return NetlifySyncFns.removeSecrets(secretSync, schemaSecretMap);
|
||||
case SecretSync.Bitbucket:
|
||||
return BitbucketSyncFns.removeSecrets(secretSync, schemaSecretMap);
|
||||
case SecretSync.LaravelForge:
|
||||
return LaravelForgeSyncFns.removeSecrets(secretSync, schemaSecretMap);
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||
|
||||
Reference in New Issue
Block a user