mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 17:27:40 +00:00
fix(vercel-integration): initial sync logic
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user