fix(vercel-integration): initial sync logic

This commit is contained in:
Daniel Hougaard
2024-12-19 01:18:47 +01:00
parent 8122433f5c
commit ed78227725
2 changed files with 150 additions and 82 deletions
@@ -1397,14 +1397,24 @@ const syncSecretsHeroku = async ({
* Sync/push [secrets] to Vercel project named [integration.app] * Sync/push [secrets] to Vercel project named [integration.app]
*/ */
const syncSecretsVercel = async ({ const syncSecretsVercel = async ({
createManySecretsRawFn,
integration, integration,
integrationAuth, integrationAuth,
secrets, secrets: infisicalSecrets,
accessToken accessToken
}: { }: {
integration: TIntegrations; createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise<Array<{ id: string }>>;
integration: TIntegrations & {
projectId: string;
environment: {
id: string;
name: string;
slug: string;
};
secretPath: string;
};
integrationAuth: TIntegrationAuths; integrationAuth: TIntegrationAuths;
secrets: Record<string, { value: string; comment?: string }>; secrets: Record<string, { value: string; comment?: string } | null>;
accessToken: string; accessToken: string;
}) => { }) => {
interface VercelSecret { interface VercelSecret {
@@ -1477,80 +1487,114 @@ const syncSecretsVercel = async ({
} }
} }
const updateSecrets: VercelSecret[] = []; const metadata = IntegrationMetadataSchema.parse(integration.metadata);
const deleteSecrets: VercelSecret[] = [];
const newSecrets: VercelSecret[] = [];
// Identify secrets to create const secretsToAddToInfisical: { [key: string]: VercelSecret } = {};
Object.keys(secrets).forEach((key) => {
if (!(key in res)) { Object.keys(res).forEach((vercelKey) => {
// case: secret has been created if (!integration.lastUsed) {
newSecrets.push({ // first time using integration
key, // -> apply initial sync behavior
value: secrets[key].value, switch (metadata.initialSyncBehavior) {
type: "encrypted", // Override all the secrets in Vercel
target: [integration.targetEnvironment as string], case IntegrationInitialSyncBehavior.OVERWRITE_TARGET: {
...(integration.path if (!(vercelKey in infisicalSecrets)) infisicalSecrets[vercelKey] = null;
? { break;
gitBranch: integration.path }
} case IntegrationInitialSyncBehavior.PREFER_SOURCE: {
: {}) // if the vercel secret is not in infisical, we need to add it to infisical
}); if (!(vercelKey in infisicalSecrets)) {
infisicalSecrets[vercelKey] = {
value: res[vercelKey].value
};
secretsToAddToInfisical[vercelKey] = res[vercelKey];
}
break;
}
default: {
throw new Error(`Invalid initial sync behavior: ${metadata.initialSyncBehavior}`);
}
}
} else if (!(vercelKey in infisicalSecrets)) {
infisicalSecrets[vercelKey] = null;
} }
}); });
// Identify secrets to update and delete if (Object.keys(secretsToAddToInfisical).length) {
Object.keys(res).forEach((key) => { await createManySecretsRawFn({
if (key in secrets) { projectId: integration.projectId,
if (res[key].value !== secrets[key].value) { environment: integration.environment.slug,
// case: secret value has changed path: integration.secretPath,
updateSecrets.push({ secrets: Object.keys(secretsToAddToInfisical).map((key) => ({
id: res[key].id, secretName: key,
key, secretValue: secretsToAddToInfisical[key].value,
value: secrets[key].value, type: SecretType.Shared,
type: res[key].type, secretComment: ""
target: res[key].target.includes(integration.targetEnvironment as string) }))
? [...res[key].target]
: [...res[key].target, integration.targetEnvironment as string],
...(integration.path
? {
gitBranch: integration.path
}
: {})
});
}
} else {
// case: secret has been deleted
deleteSecrets.push({
id: res[key].id,
key,
value: res[key].value,
type: "encrypted", // value doesn't matter
target: [integration.targetEnvironment as string],
...(integration.path
? {
gitBranch: integration.path
}
: {})
});
}
});
// Sync/push new secrets
if (newSecrets.length > 0) {
await request.post(`${IntegrationUrls.VERCEL_API_URL}/v10/projects/${integration.app}/env`, newSecrets, {
params,
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
}); });
} }
for await (const secret of updateSecrets) { // update and create logic
if (secret.type !== "sensitive") { for await (const key of Object.keys(infisicalSecrets)) {
const { id, ...updatedSecret } = secret; if (!(key in res) || infisicalSecrets[key]?.value !== res[key].value) {
await request.patch(`${IntegrationUrls.VERCEL_API_URL}/v9/projects/${integration.app}/env/${id}`, updatedSecret, { // if the key is not in the vercel res, we need to create it
if (!(key in res)) {
await request.post(
`${IntegrationUrls.VERCEL_API_URL}/v10/projects/${integration.app}/env`,
{
key,
value: infisicalSecrets[key]?.value,
type: "encrypted",
target: [integration.targetEnvironment as string],
...(integration.path
? {
gitBranch: integration.path
}
: {})
},
{
params,
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
}
);
// Else if the key already exists and its not sensitive, we need to update it
} else if (res[key].type !== "sensitive") {
await request.patch(
`${IntegrationUrls.VERCEL_API_URL}/v9/projects/${integration.app}/env/${res[key].id}`,
{
key,
value: infisicalSecrets[key]?.value,
type: res[key].type,
target: res[key].target.includes(integration.targetEnvironment as string)
? [...res[key].target]
: [...res[key].target, integration.targetEnvironment as string],
...(integration.path
? {
gitBranch: integration.path
}
: {})
},
{
params,
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
}
);
}
}
}
// delete logic
for await (const key of Object.keys(res)) {
if (infisicalSecrets[key] === null) {
// case: delete secret
await request.delete(`${IntegrationUrls.VERCEL_API_URL}/v9/projects/${integration.app}/env/${res[key].id}`, {
params, params,
headers: { headers: {
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
@@ -1559,16 +1603,6 @@ const syncSecretsVercel = async ({
}); });
} }
} }
for await (const secret of deleteSecrets) {
await request.delete(`${IntegrationUrls.VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, {
params,
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
});
}
}; };
/** /**
@@ -4471,7 +4505,8 @@ export const syncIntegrationSecrets = async ({
integration, integration,
integrationAuth, integrationAuth,
secrets, secrets,
accessToken accessToken,
createManySecretsRawFn
}); });
break; break;
case Integrations.NETLIFY: case Integrations.NETLIFY:
@@ -13,6 +13,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import queryString from "query-string"; import queryString from "query-string";
import { useCreateIntegration } from "@app/hooks/api"; import { useCreateIntegration } from "@app/hooks/api";
import { IntegrationSyncBehavior } from "@app/hooks/api/integrations/types";
import { import {
Button, Button,
@@ -36,12 +37,26 @@ const vercelEnvironments = [
{ name: "Production", slug: "production" } { name: "Production", slug: "production" }
]; ];
const initialSyncBehaviors = [
{
label: "No Import - Overwrite all values in Vercel",
value: IntegrationSyncBehavior.OVERWRITE_TARGET
},
{
label: "Import - Prefer values from Infisical",
value: IntegrationSyncBehavior.PREFER_SOURCE
}
];
export default function VercelCreateIntegrationPage() { export default function VercelCreateIntegrationPage() {
const router = useRouter(); const router = useRouter();
const { mutateAsync } = useCreateIntegration(); const { mutateAsync } = useCreateIntegration();
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(""); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
const [secretPath, setSecretPath] = useState("/"); const [secretPath, setSecretPath] = useState("/");
const [initialSyncBehavior, setInitialSyncBehavior] = useState<IntegrationSyncBehavior>(
IntegrationSyncBehavior.OVERWRITE_TARGET
);
const [targetAppId, setTargetAppId] = useState(""); const [targetAppId, setTargetAppId] = useState("");
const [targetEnvironment, setTargetEnvironment] = useState(""); const [targetEnvironment, setTargetEnvironment] = useState("");
const [targetBranch, setTargetBranch] = useState(""); const [targetBranch, setTargetBranch] = useState("");
@@ -104,7 +119,10 @@ export default function VercelCreateIntegrationPage() {
sourceEnvironment: selectedSourceEnvironment, sourceEnvironment: selectedSourceEnvironment,
targetEnvironment, targetEnvironment,
path, path,
secretPath secretPath,
metadata: {
initialSyncBehavior
}
}); });
setIsLoading(false); setIsLoading(false);
@@ -231,6 +249,21 @@ export default function VercelCreateIntegrationPage() {
</Select> </Select>
</FormControl> </FormControl>
)} )}
<FormControl label="Initial Sync Behavior" className="px-6">
<Select
value={initialSyncBehavior}
onValueChange={(val) => setInitialSyncBehavior(val as IntegrationSyncBehavior)}
className="w-full border border-mineshaft-500 text-sm"
>
{initialSyncBehaviors.map((syncBehavior) => (
<SelectItem value={syncBehavior.value} key={`sync-behavior-${syncBehavior.value}`}>
{syncBehavior.label}
</SelectItem>
))}
</Select>
</FormControl>
<Button <Button
onClick={handleButtonClick} onClick={handleButtonClick}
color="mineshaft" color="mineshaft"