Improve render api usage

This commit is contained in:
Carlos Monastyrski
2025-07-24 18:51:44 -03:00
parent eadb1a63fa
commit c031736701
@@ -10,20 +10,24 @@ import { TRenderSecret, TRenderSyncWithCredentials } from "./render-sync-types";
const MAX_RETRIES = 5; const MAX_RETRIES = 5;
const sleep = async () =>
new Promise((resolve) => {
setTimeout(resolve, 2000);
});
const retrySleep = async () => const retrySleep = async () =>
new Promise((resolve) => { new Promise((resolve) => {
setTimeout(resolve, 60000); setTimeout(resolve, 60000);
}); });
const getRenderEnvironmentSecrets = async ( const makeRequestWithRetry = async <T>(requestFn: () => Promise<T>, attempt = 0): Promise<T> => {
secretSync: TRenderSyncWithCredentials, try {
attempt = 0 return await requestFn();
): Promise<TRenderSecret[]> => { } catch (error) {
if (isAxiosError(error) && error.response?.status === 429 && attempt < MAX_RETRIES) {
await retrySleep();
return await makeRequestWithRetry(requestFn, attempt + 1);
}
throw error;
}
};
const getRenderEnvironmentSecrets = async (secretSync: TRenderSyncWithCredentials): Promise<TRenderSecret[]> => {
const { const {
destinationConfig, destinationConfig,
connection: { connection: {
@@ -38,8 +42,8 @@ const getRenderEnvironmentSecrets = async (
do { do {
const url = cursor ? `${baseUrl}?cursor=${cursor}` : baseUrl; const url = cursor ? `${baseUrl}?cursor=${cursor}` : baseUrl;
try { const { data } = await makeRequestWithRetry(() =>
const { data } = await request.get< request.get<
{ {
envVar: { envVar: {
key: string; key: string;
@@ -52,32 +56,29 @@ const getRenderEnvironmentSecrets = async (
Authorization: `Bearer ${apiKey}`, Authorization: `Bearer ${apiKey}`,
Accept: "application/json" Accept: "application/json"
} }
}); })
);
const secrets = data.map((item) => ({ const secrets = data.map((item) => ({
key: item.envVar.key, key: item.envVar.key,
value: item.envVar.value value: item.envVar.value
})); }));
allSecrets.push(...secrets); allSecrets.push(...secrets);
cursor = data[data.length - 1]?.cursor;
} catch (error) { if (data.length > 0 && data[data.length - 1]?.cursor) {
if (isAxiosError(error) && error.response?.status === 429 && attempt < MAX_RETRIES) { cursor = data[data.length - 1].cursor;
await retrySleep(); } else {
return await getRenderEnvironmentSecrets(secretSync, attempt + 1); cursor = undefined;
}
throw error;
} }
} while (cursor); } while (cursor);
return allSecrets; return allSecrets;
}; };
const putEnvironmentSecret = async ( const batchUpdateEnvironmentSecrets = async (
secretSync: TRenderSyncWithCredentials, secretSync: TRenderSyncWithCredentials,
secretMap: TSecretMap, envVars: Array<{ key: string; value: string }>
key: string,
attempt = 0
): Promise<void> => { ): Promise<void> => {
const { const {
destinationConfig, destinationConfig,
@@ -86,92 +87,50 @@ const putEnvironmentSecret = async (
} }
} = secretSync; } = secretSync;
try { await makeRequestWithRetry(() =>
await request.put( request.put(`${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/env-vars`, envVars, {
`${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/env-vars/${key}`, headers: {
{ Authorization: `Bearer ${apiKey}`,
key, Accept: "application/json"
value: secretMap[key].value
},
{
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json"
}
} }
); })
} catch (error) { );
if (isAxiosError(error) && error.response?.status === 429 && attempt < MAX_RETRIES) {
await retrySleep();
return await putEnvironmentSecret(secretSync, secretMap, key, attempt + 1);
}
throw error;
}
};
const deleteEnvironmentSecret = async (
secretSync: TRenderSyncWithCredentials,
secret: Pick<TRenderSecret, "key">,
attempt = 0
): Promise<void> => {
const {
destinationConfig,
connection: {
credentials: { apiKey }
}
} = secretSync;
try {
await request.delete(
`${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/env-vars/${secret.key}`,
{
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json"
}
}
);
} catch (error) {
if (isAxiosError(error) && error.response?.status === 404) {
// If the secret does not exist, we can ignore this error
return;
}
if (isAxiosError(error) && error.response?.status === 429 && attempt < MAX_RETRIES) {
await retrySleep();
return await deleteEnvironmentSecret(secretSync, secret, attempt + 1);
}
throw error;
}
}; };
export const RenderSyncFns = { export const RenderSyncFns = {
syncSecrets: async (secretSync: TRenderSyncWithCredentials, secretMap: TSecretMap) => { syncSecrets: async (secretSync: TRenderSyncWithCredentials, secretMap: TSecretMap) => {
const renderSecrets = await getRenderEnvironmentSecrets(secretSync); const renderSecrets = await getRenderEnvironmentSecrets(secretSync);
for await (const key of Object.keys(secretMap)) { const finalEnvVars: Array<{ key: string; value: string }> = [];
// If value is empty skip it as render does not allow empty variables
if (secretMap[key].value === "") { for (const renderSecret of renderSecrets) {
const shouldKeep =
secretMap[renderSecret.key] ||
(secretSync.syncOptions.disableSecretDeletion &&
!matchesSchema(renderSecret.key, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema));
if (shouldKeep && !secretMap[renderSecret.key]) {
finalEnvVars.push({
key: renderSecret.key,
value: renderSecret.value
});
}
}
for (const [key, secret] of Object.entries(secretMap)) {
// Skip empty values as render does not allow empty variables
if (secret.value === "") {
// eslint-disable-next-line no-continue // eslint-disable-next-line no-continue
continue; continue;
} }
await putEnvironmentSecret(secretSync, secretMap, key);
await sleep(); finalEnvVars.push({
key,
value: secret.value
});
} }
if (secretSync.syncOptions.disableSecretDeletion) return; await batchUpdateEnvironmentSecrets(secretSync, finalEnvVars);
for await (const renderSecret of renderSecrets) {
if (!matchesSchema(renderSecret.key, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema))
// eslint-disable-next-line no-continue
continue;
if (!secretMap[renderSecret.key]) {
await deleteEnvironmentSecret(secretSync, renderSecret);
await sleep();
}
}
}, },
getSecrets: async (secretSync: TRenderSyncWithCredentials): Promise<TSecretMap> => { getSecrets: async (secretSync: TRenderSyncWithCredentials): Promise<TSecretMap> => {
@@ -180,13 +139,17 @@ export const RenderSyncFns = {
}, },
removeSecrets: async (secretSync: TRenderSyncWithCredentials, secretMap: TSecretMap) => { removeSecrets: async (secretSync: TRenderSyncWithCredentials, secretMap: TSecretMap) => {
const encryptedSecrets = await getRenderEnvironmentSecrets(secretSync); const renderSecrets = await getRenderEnvironmentSecrets(secretSync);
const finalEnvVars: Array<{ key: string; value: string }> = [];
for await (const encryptedSecret of encryptedSecrets) { for (const renderSecret of renderSecrets) {
if (encryptedSecret.key in secretMap) { if (!(renderSecret.key in secretMap)) {
await deleteEnvironmentSecret(secretSync, encryptedSecret); finalEnvVars.push({
await sleep(); key: renderSecret.key,
value: renderSecret.value
});
} }
} }
await batchUpdateEnvironmentSecrets(secretSync, finalEnvVars);
} }
}; };