mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat: support render environment groups (#4327)
* feat: support env groups in render sync * fix: update doc * Update backend/src/services/app-connection/render/render-connection-service.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * fix: pr changes * fix: lint and type check * fix: changes * fix: remove secrets * fix: MAX iterations in render sync * fix: render sync review fields * fix: pr changes * fix: lint * fix: changes --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
@@ -2491,6 +2491,7 @@ export const SecretSyncs = {
|
||||
},
|
||||
RENDER: {
|
||||
serviceId: "The ID of the Render service to sync secrets to.",
|
||||
environmentGroupId: "The ID of the Render environment group to sync secrets to.",
|
||||
scope: "The Render scope that secrets should be synced to.",
|
||||
type: "The Render resource type to sync secrets to."
|
||||
},
|
||||
|
||||
@@ -49,4 +49,32 @@ export const registerRenderConnectionRouter = async (server: FastifyZodProvider)
|
||||
return services;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/:connectionId/environment-groups`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
connectionId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string()
|
||||
})
|
||||
.array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { connectionId } = req.params;
|
||||
const groups = await server.services.appConnection.render.listEnvironmentGroups(connectionId, req.permission);
|
||||
|
||||
return groups;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -8,9 +8,11 @@ import { IntegrationUrls } from "@app/services/integration-auth/integration-list
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { RenderConnectionMethod } from "./render-connection-enums";
|
||||
import {
|
||||
TRawRenderEnvironmentGroup,
|
||||
TRawRenderService,
|
||||
TRenderConnection,
|
||||
TRenderConnectionConfig,
|
||||
TRenderEnvironmentGroup,
|
||||
TRenderService
|
||||
} from "./render-connection-types";
|
||||
|
||||
@@ -32,7 +34,11 @@ export const listRenderServices = async (appConnection: TRenderConnection): Prom
|
||||
const perPage = 100;
|
||||
let cursor;
|
||||
|
||||
let maxIterations = 10;
|
||||
|
||||
while (hasMorePages) {
|
||||
if (maxIterations <= 0) break;
|
||||
|
||||
const res: TRawRenderService[] = (
|
||||
await request.get<TRawRenderService[]>(`${IntegrationUrls.RENDER_API_URL}/v1/services`, {
|
||||
params: new URLSearchParams({
|
||||
@@ -59,6 +65,8 @@ export const listRenderServices = async (appConnection: TRenderConnection): Prom
|
||||
} else {
|
||||
cursor = res[res.length - 1].cursor;
|
||||
}
|
||||
|
||||
maxIterations -= 1;
|
||||
}
|
||||
|
||||
return services;
|
||||
@@ -86,3 +94,52 @@ export const validateRenderConnectionCredentials = async (config: TRenderConnect
|
||||
|
||||
return inputCredentials;
|
||||
};
|
||||
|
||||
export const listRenderEnvironmentGroups = async (
|
||||
appConnection: TRenderConnection
|
||||
): Promise<TRenderEnvironmentGroup[]> => {
|
||||
const {
|
||||
credentials: { apiKey }
|
||||
} = appConnection;
|
||||
|
||||
const groups: TRenderEnvironmentGroup[] = [];
|
||||
let hasMorePages = true;
|
||||
const perPage = 100;
|
||||
let cursor;
|
||||
let maxIterations = 10;
|
||||
|
||||
while (hasMorePages) {
|
||||
if (maxIterations <= 0) break;
|
||||
|
||||
const res: TRawRenderEnvironmentGroup[] = (
|
||||
await request.get<TRawRenderEnvironmentGroup[]>(`${IntegrationUrls.RENDER_API_URL}/v1/env-groups`, {
|
||||
params: new URLSearchParams({
|
||||
...(cursor ? { cursor: String(cursor) } : {}),
|
||||
limit: String(perPage)
|
||||
}),
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
Accept: "application/json",
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
})
|
||||
).data;
|
||||
|
||||
res.forEach((item) => {
|
||||
groups.push({
|
||||
name: item.envGroup.name,
|
||||
id: item.envGroup.id
|
||||
});
|
||||
});
|
||||
|
||||
if (res.length < perPage) {
|
||||
hasMorePages = false;
|
||||
} else {
|
||||
cursor = res[res.length - 1].cursor;
|
||||
}
|
||||
|
||||
maxIterations -= 1;
|
||||
}
|
||||
|
||||
return groups;
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import { logger } from "@app/lib/logger";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { listRenderServices } from "./render-connection-fns";
|
||||
import { listRenderEnvironmentGroups, listRenderServices } from "./render-connection-fns";
|
||||
import { TRenderConnection } from "./render-connection-types";
|
||||
|
||||
type TGetAppConnectionFunc = (
|
||||
@@ -24,7 +24,20 @@ export const renderConnectionService = (getAppConnection: TGetAppConnectionFunc)
|
||||
}
|
||||
};
|
||||
|
||||
const listEnvironmentGroups = async (connectionId: string, actor: OrgServiceActor) => {
|
||||
const appConnection = await getAppConnection(AppConnection.Render, connectionId, actor);
|
||||
try {
|
||||
const groups = await listRenderEnvironmentGroups(appConnection);
|
||||
|
||||
return groups;
|
||||
} catch (error) {
|
||||
logger.error(error, "Failed to list environment groups for Render connection");
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
listServices
|
||||
listServices,
|
||||
listEnvironmentGroups
|
||||
};
|
||||
};
|
||||
|
||||
@@ -33,3 +33,16 @@ export type TRawRenderService = {
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TRenderEnvironmentGroup = {
|
||||
name: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type TRawRenderEnvironmentGroup = {
|
||||
cursor: string;
|
||||
envGroup: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export enum RenderSyncScope {
|
||||
Service = "service"
|
||||
Service = "service",
|
||||
EnvironmentGroup = "environment-group"
|
||||
}
|
||||
|
||||
export enum RenderSyncType {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import { isAxiosError } from "axios";
|
||||
import { AxiosRequestConfig, isAxiosError } from "axios";
|
||||
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
|
||||
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
import { RenderSyncScope } from "./render-sync-enums";
|
||||
import { TRenderSecret, TRenderSyncWithCredentials } from "./render-sync-types";
|
||||
|
||||
const MAX_RETRIES = 5;
|
||||
@@ -27,6 +29,80 @@ const makeRequestWithRetry = async <T>(requestFn: () => Promise<T>, attempt = 0)
|
||||
}
|
||||
};
|
||||
|
||||
async function getSecrets(input: { destination: TRenderSyncWithCredentials["destinationConfig"]; token: string }) {
|
||||
const req: AxiosRequestConfig = {
|
||||
baseURL: `${IntegrationUrls.RENDER_API_URL}/v1`,
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${input.token}`,
|
||||
Accept: "application/json"
|
||||
}
|
||||
};
|
||||
|
||||
switch (input.destination.scope) {
|
||||
case RenderSyncScope.Service: {
|
||||
req.url = `/services/${input.destination.serviceId}/env-vars`;
|
||||
|
||||
const allSecrets: TRenderSecret[] = [];
|
||||
let cursor: string | undefined;
|
||||
|
||||
do {
|
||||
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
||||
const { data } = await makeRequestWithRetry(() =>
|
||||
request.request<
|
||||
{
|
||||
envVar: {
|
||||
key: string;
|
||||
value: string;
|
||||
};
|
||||
cursor: string;
|
||||
}[]
|
||||
>({
|
||||
...req,
|
||||
params: {
|
||||
cursor
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const secrets = data.map((item) => ({
|
||||
key: item.envVar.key,
|
||||
value: item.envVar.value
|
||||
}));
|
||||
|
||||
allSecrets.push(...secrets);
|
||||
|
||||
if (data.length > 0 && data[data.length - 1]?.cursor) {
|
||||
cursor = data[data.length - 1].cursor;
|
||||
} else {
|
||||
cursor = undefined;
|
||||
}
|
||||
} while (cursor);
|
||||
|
||||
return allSecrets;
|
||||
}
|
||||
case RenderSyncScope.EnvironmentGroup: {
|
||||
req.url = `/env-groups/${input.destination.environmentGroupId}`;
|
||||
|
||||
const res = await makeRequestWithRetry(() =>
|
||||
request.request<{
|
||||
envVars: {
|
||||
key: string;
|
||||
value: string;
|
||||
}[];
|
||||
}>(req)
|
||||
);
|
||||
|
||||
return res.data.envVars.map((item) => ({
|
||||
key: item.key,
|
||||
value: item.value
|
||||
}));
|
||||
}
|
||||
default:
|
||||
throw new BadRequestError({ message: "Unknown render sync destination scope" });
|
||||
}
|
||||
}
|
||||
|
||||
const getRenderEnvironmentSecrets = async (secretSync: TRenderSyncWithCredentials): Promise<TRenderSecret[]> => {
|
||||
const {
|
||||
destinationConfig,
|
||||
@@ -35,45 +111,12 @@ const getRenderEnvironmentSecrets = async (secretSync: TRenderSyncWithCredential
|
||||
}
|
||||
} = secretSync;
|
||||
|
||||
const baseUrl = `${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/env-vars`;
|
||||
const allSecrets: TRenderSecret[] = [];
|
||||
let cursor: string | undefined;
|
||||
const secrets = await getSecrets({
|
||||
destination: destinationConfig,
|
||||
token: apiKey
|
||||
});
|
||||
|
||||
do {
|
||||
const url = cursor ? `${baseUrl}?cursor=${cursor}` : baseUrl;
|
||||
|
||||
const { data } = await makeRequestWithRetry(() =>
|
||||
request.get<
|
||||
{
|
||||
envVar: {
|
||||
key: string;
|
||||
value: string;
|
||||
};
|
||||
cursor: string;
|
||||
}[]
|
||||
>(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
Accept: "application/json"
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const secrets = data.map((item) => ({
|
||||
key: item.envVar.key,
|
||||
value: item.envVar.value
|
||||
}));
|
||||
|
||||
allSecrets.push(...secrets);
|
||||
|
||||
if (data.length > 0 && data[data.length - 1]?.cursor) {
|
||||
cursor = data[data.length - 1].cursor;
|
||||
} else {
|
||||
cursor = undefined;
|
||||
}
|
||||
} while (cursor);
|
||||
|
||||
return allSecrets;
|
||||
return secrets;
|
||||
};
|
||||
|
||||
const batchUpdateEnvironmentSecrets = async (
|
||||
@@ -87,14 +130,91 @@ const batchUpdateEnvironmentSecrets = async (
|
||||
}
|
||||
} = secretSync;
|
||||
|
||||
await makeRequestWithRetry(() =>
|
||||
request.put(`${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/env-vars`, envVars, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
Accept: "application/json"
|
||||
const req: AxiosRequestConfig = {
|
||||
baseURL: `${IntegrationUrls.RENDER_API_URL}/v1`,
|
||||
method: "PUT",
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
Accept: "application/json"
|
||||
}
|
||||
};
|
||||
|
||||
switch (destinationConfig.scope) {
|
||||
case RenderSyncScope.Service: {
|
||||
await makeRequestWithRetry(() =>
|
||||
request.request({
|
||||
...req,
|
||||
url: `/services/${destinationConfig.serviceId}/env-vars`,
|
||||
data: envVars
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case RenderSyncScope.EnvironmentGroup: {
|
||||
for await (const variable of envVars) {
|
||||
await makeRequestWithRetry(() =>
|
||||
request.request({
|
||||
...req,
|
||||
url: `/env-groups/${destinationConfig.environmentGroupId}/env-vars/${variable.key}`,
|
||||
data: {
|
||||
value: variable.value
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new BadRequestError({ message: "Unknown render sync destination scope" });
|
||||
}
|
||||
};
|
||||
|
||||
const deleteEnvironmentSecret = async (
|
||||
secretSync: TRenderSyncWithCredentials,
|
||||
envVar: { key: string; value: string }
|
||||
): Promise<void> => {
|
||||
const {
|
||||
destinationConfig,
|
||||
connection: {
|
||||
credentials: { apiKey }
|
||||
}
|
||||
} = secretSync;
|
||||
|
||||
const req: AxiosRequestConfig = {
|
||||
baseURL: `${IntegrationUrls.RENDER_API_URL}/v1`,
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
Accept: "application/json"
|
||||
}
|
||||
};
|
||||
|
||||
switch (destinationConfig.scope) {
|
||||
case RenderSyncScope.Service: {
|
||||
await makeRequestWithRetry(() =>
|
||||
request.request({
|
||||
...req,
|
||||
url: `/services/${destinationConfig.serviceId}/env-vars/${envVar.key}`
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case RenderSyncScope.EnvironmentGroup: {
|
||||
await makeRequestWithRetry(() =>
|
||||
request.request({
|
||||
...req,
|
||||
url: `/env-groups/${destinationConfig.environmentGroupId}/env-vars/${envVar.key}`
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new BadRequestError({ message: "Unknown render sync destination scope" });
|
||||
}
|
||||
};
|
||||
|
||||
const redeployService = async (secretSync: TRenderSyncWithCredentials) => {
|
||||
@@ -105,18 +225,50 @@ const redeployService = async (secretSync: TRenderSyncWithCredentials) => {
|
||||
}
|
||||
} = secretSync;
|
||||
|
||||
await makeRequestWithRetry(() =>
|
||||
request.post(
|
||||
`${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/deploys`,
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
Accept: "application/json"
|
||||
}
|
||||
const req: AxiosRequestConfig = {
|
||||
baseURL: `${IntegrationUrls.RENDER_API_URL}/v1`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
Accept: "application/json"
|
||||
}
|
||||
};
|
||||
|
||||
switch (destinationConfig.scope) {
|
||||
case RenderSyncScope.Service: {
|
||||
await makeRequestWithRetry(() =>
|
||||
request.request({
|
||||
...req,
|
||||
method: "POST",
|
||||
url: `/services/${destinationConfig.serviceId}/deploys`,
|
||||
data: {}
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case RenderSyncScope.EnvironmentGroup: {
|
||||
const { data } = await request.request<{ serviceLinks: { id: string }[] }>({
|
||||
...req,
|
||||
method: "GET",
|
||||
url: `/env-groups/${destinationConfig.environmentGroupId}`
|
||||
});
|
||||
|
||||
for await (const link of data.serviceLinks) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
||||
await makeRequestWithRetry(() =>
|
||||
request.request({
|
||||
...req,
|
||||
url: `/services/${link.id}/deploys`,
|
||||
data: {}
|
||||
})
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new BadRequestError({ message: "Unknown render sync destination scope" });
|
||||
}
|
||||
};
|
||||
|
||||
export const RenderSyncFns = {
|
||||
@@ -169,14 +321,15 @@ export const RenderSyncFns = {
|
||||
const finalEnvVars: Array<{ key: string; value: string }> = [];
|
||||
|
||||
for (const renderSecret of renderSecrets) {
|
||||
if (!(renderSecret.key in secretMap)) {
|
||||
if (renderSecret.key in secretMap) {
|
||||
finalEnvVars.push({
|
||||
key: renderSecret.key,
|
||||
value: renderSecret.value
|
||||
});
|
||||
}
|
||||
}
|
||||
await batchUpdateEnvironmentSecrets(secretSync, finalEnvVars);
|
||||
|
||||
await Promise.all(finalEnvVars.map((el) => deleteEnvironmentSecret(secretSync, el)));
|
||||
|
||||
if (secretSync.syncOptions.autoRedeployServices) {
|
||||
await redeployService(secretSync);
|
||||
|
||||
@@ -17,6 +17,14 @@ const RenderSyncDestinationConfigSchema = z.discriminatedUnion("scope", [
|
||||
scope: z.literal(RenderSyncScope.Service).describe(SecretSyncs.DESTINATION_CONFIG.RENDER.scope),
|
||||
serviceId: z.string().min(1, "Service ID is required").describe(SecretSyncs.DESTINATION_CONFIG.RENDER.serviceId),
|
||||
type: z.nativeEnum(RenderSyncType).describe(SecretSyncs.DESTINATION_CONFIG.RENDER.type)
|
||||
}),
|
||||
z.object({
|
||||
scope: z.literal(RenderSyncScope.EnvironmentGroup).describe(SecretSyncs.DESTINATION_CONFIG.RENDER.scope),
|
||||
environmentGroupId: z
|
||||
.string()
|
||||
.min(1, "Environment Group ID is required")
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.RENDER.environmentGroupId),
|
||||
type: z.nativeEnum(RenderSyncType).describe(SecretSyncs.DESTINATION_CONFIG.RENDER.type)
|
||||
})
|
||||
]);
|
||||
|
||||
|
||||
@@ -30,8 +30,9 @@ description: "Learn how to configure a Render Sync for Infisical."
|
||||

|
||||
|
||||
- **Render Connection**: The Render Connection to authenticate with.
|
||||
- **Scope**: Select **Service**.
|
||||
- **Service**: Choose the Render service you want to sync secrets to.
|
||||
- **Scope**: Select **Service** or **Environment Group**.
|
||||
- **Service**: Choose the Render service you want to sync secrets to.
|
||||
- **Environment Group**: Choose the Render environment group you want to sync secrets to.
|
||||
|
||||
5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**.
|
||||

|
||||
|
||||
@@ -5,7 +5,9 @@ import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/Se
|
||||
import { FilterableSelect, FormControl, Select, SelectItem } from "@app/components/v2";
|
||||
import { RENDER_SYNC_SCOPES } from "@app/helpers/secretSyncs";
|
||||
import {
|
||||
TRenderEnvironmentGroup,
|
||||
TRenderService,
|
||||
useRenderConnectionListEnvironmentGroups,
|
||||
useRenderConnectionListServices
|
||||
} from "@app/hooks/api/appConnections/render";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
@@ -19,6 +21,7 @@ export const RenderSyncFields = () => {
|
||||
>();
|
||||
|
||||
const connectionId = useWatch({ name: "connection.id", control });
|
||||
const selectedScope = useWatch({ name: "destinationConfig.scope", control });
|
||||
|
||||
const { data: services = [], isPending: isServicesPending } = useRenderConnectionListServices(
|
||||
connectionId,
|
||||
@@ -27,11 +30,17 @@ export const RenderSyncFields = () => {
|
||||
}
|
||||
);
|
||||
|
||||
const { data: groups = [], isPending: isGroupsPending } =
|
||||
useRenderConnectionListEnvironmentGroups(connectionId, {
|
||||
enabled: Boolean(connectionId) && selectedScope === RenderSyncScope.EnvironmentGroup
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<SecretSyncConnectionField
|
||||
onChange={() => {
|
||||
setValue("destinationConfig.serviceId", "");
|
||||
setValue("destinationConfig.environmentGroupId", "");
|
||||
setValue("destinationConfig.type", RenderSyncType.Env);
|
||||
setValue("destinationConfig.scope", RenderSyncScope.Service);
|
||||
}}
|
||||
@@ -83,30 +92,67 @@ export const RenderSyncFields = () => {
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="destinationConfig.serviceId"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl errorText={error?.message} isError={Boolean(error?.message)} label="Service">
|
||||
<FilterableSelect
|
||||
isLoading={isServicesPending && Boolean(connectionId)}
|
||||
isDisabled={!connectionId}
|
||||
value={services ? (services.find((service) => service.id === value) ?? []) : []}
|
||||
onChange={(option) => {
|
||||
onChange((option as SingleValue<TRenderService>)?.id ?? null);
|
||||
setValue(
|
||||
"destinationConfig.serviceName",
|
||||
(option as SingleValue<TRenderService>)?.name ?? ""
|
||||
);
|
||||
}}
|
||||
options={services}
|
||||
placeholder="Select a service..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.id.toString()}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{selectedScope === RenderSyncScope.Service && (
|
||||
<Controller
|
||||
name="destinationConfig.serviceId"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Service"
|
||||
>
|
||||
<FilterableSelect
|
||||
isLoading={isServicesPending && Boolean(connectionId)}
|
||||
isDisabled={!connectionId}
|
||||
value={services ? (services.find((service) => service.id === value) ?? []) : []}
|
||||
onChange={(option) => {
|
||||
onChange((option as SingleValue<TRenderService>)?.id ?? null);
|
||||
setValue(
|
||||
"destinationConfig.serviceName",
|
||||
(option as SingleValue<TRenderService>)?.name ?? ""
|
||||
);
|
||||
}}
|
||||
options={services}
|
||||
placeholder="Select a service..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.id.toString()}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedScope === RenderSyncScope.EnvironmentGroup && (
|
||||
<Controller
|
||||
name="destinationConfig.environmentGroupId"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Environment Group"
|
||||
>
|
||||
<FilterableSelect
|
||||
isLoading={isGroupsPending && Boolean(connectionId)}
|
||||
isDisabled={!connectionId}
|
||||
value={groups ? (groups.find((g) => g.id === value) ?? []) : []}
|
||||
onChange={(option) => {
|
||||
onChange((option as SingleValue<TRenderEnvironmentGroup>)?.id ?? null);
|
||||
setValue(
|
||||
"destinationConfig.environmentGroupName",
|
||||
(option as SingleValue<TRenderEnvironmentGroup>)?.name ?? ""
|
||||
);
|
||||
}}
|
||||
options={groups}
|
||||
placeholder="Select an environment group..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.id.toString()}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import { GenericFieldLabel } from "@app/components/secret-syncs";
|
||||
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
||||
import { Badge } from "@app/components/v2";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
import { RenderSyncScope } from "@app/hooks/api/secretSyncs/types/render-sync";
|
||||
|
||||
export const RenderSyncOptionsReviewFields = () => {
|
||||
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Render }>();
|
||||
@@ -27,13 +28,20 @@ export const RenderSyncOptionsReviewFields = () => {
|
||||
|
||||
export const RenderSyncReviewFields = () => {
|
||||
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Render }>();
|
||||
const serviceName = watch("destinationConfig.serviceName");
|
||||
const scope = watch("destinationConfig.scope");
|
||||
const config = watch("destinationConfig");
|
||||
|
||||
return (
|
||||
<>
|
||||
<GenericFieldLabel label="Scope">{scope}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Service">{serviceName}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Scope">{config.scope}</GenericFieldLabel>
|
||||
{config.scope === RenderSyncScope.Service ? (
|
||||
<GenericFieldLabel label="Service">
|
||||
{config.serviceName ?? config.serviceId}
|
||||
</GenericFieldLabel>
|
||||
) : (
|
||||
<GenericFieldLabel label="Service">
|
||||
{config.environmentGroupName ?? config.environmentGroupId}
|
||||
</GenericFieldLabel>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,6 +17,12 @@ export const RenderSyncDestinationSchema = BaseSecretSyncSchema(
|
||||
serviceId: z.string().trim().min(1, "Service is required"),
|
||||
serviceName: z.string().trim().optional(),
|
||||
type: z.nativeEnum(RenderSyncType)
|
||||
}),
|
||||
z.object({
|
||||
scope: z.literal(RenderSyncScope.EnvironmentGroup),
|
||||
environmentGroupId: z.string().trim().min(1, "Environment Group ID is required"),
|
||||
environmentGroupName: z.string().trim().optional(),
|
||||
type: z.nativeEnum(RenderSyncType)
|
||||
})
|
||||
])
|
||||
})
|
||||
|
||||
@@ -212,5 +212,9 @@ export const RENDER_SYNC_SCOPES: Record<RenderSyncScope, { name: string; descrip
|
||||
[RenderSyncScope.Service]: {
|
||||
name: "Service",
|
||||
description: "Infisical will sync secrets to the specified Render service."
|
||||
},
|
||||
[RenderSyncScope.EnvironmentGroup]: {
|
||||
name: "EnvironmentGroup",
|
||||
description: "Infisical will sync secrets to the specified Render environment group."
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,12 +3,14 @@ import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { appConnectionKeys } from "../queries";
|
||||
import { TRenderService } from "./types";
|
||||
import { TRenderEnvironmentGroup, TRenderService } from "./types";
|
||||
|
||||
const renderConnectionKeys = {
|
||||
all: [...appConnectionKeys.all, "render"] as const,
|
||||
listServices: (connectionId: string) =>
|
||||
[...renderConnectionKeys.all, "services", connectionId] as const
|
||||
[...renderConnectionKeys.all, "services", connectionId] as const,
|
||||
listEnvironmentGroups: (connectionId: string) =>
|
||||
[...renderConnectionKeys.all, "environment-groups", connectionId] as const
|
||||
};
|
||||
|
||||
export const useRenderConnectionListServices = (
|
||||
@@ -35,3 +37,28 @@ export const useRenderConnectionListServices = (
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
export const useRenderConnectionListEnvironmentGroups = (
|
||||
connectionId: string,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
TRenderEnvironmentGroup[],
|
||||
unknown,
|
||||
TRenderEnvironmentGroup[],
|
||||
ReturnType<typeof renderConnectionKeys.listEnvironmentGroups>
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: renderConnectionKeys.listEnvironmentGroups(connectionId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<TRenderEnvironmentGroup[]>(
|
||||
`/api/v1/app-connections/render/${connectionId}/environment-groups`
|
||||
);
|
||||
|
||||
return data;
|
||||
},
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
@@ -2,3 +2,8 @@ export type TRenderService = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type TRenderEnvironmentGroup = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
@@ -4,12 +4,19 @@ import { RootSyncOptions, TRootSecretSync } from "@app/hooks/api/secretSyncs/typ
|
||||
|
||||
export type TRenderSync = TRootSecretSync & {
|
||||
destination: SecretSync.Render;
|
||||
destinationConfig: {
|
||||
scope: RenderSyncScope.Service;
|
||||
type: RenderSyncType;
|
||||
serviceId: string;
|
||||
serviceName?: string;
|
||||
};
|
||||
destinationConfig:
|
||||
| {
|
||||
type: RenderSyncType;
|
||||
scope: RenderSyncScope.Service;
|
||||
serviceId: string;
|
||||
serviceName?: string | undefined;
|
||||
}
|
||||
| {
|
||||
type: RenderSyncType;
|
||||
scope: RenderSyncScope.EnvironmentGroup;
|
||||
environmentGroupId: string;
|
||||
environmentGroupName?: string | undefined;
|
||||
};
|
||||
|
||||
connection: {
|
||||
app: AppConnection.Render;
|
||||
@@ -23,7 +30,8 @@ export type TRenderSync = TRootSecretSync & {
|
||||
};
|
||||
|
||||
export enum RenderSyncScope {
|
||||
Service = "service"
|
||||
Service = "service",
|
||||
EnvironmentGroup = "environment-group"
|
||||
}
|
||||
|
||||
export enum RenderSyncType {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { useRenderConnectionListServices } from "@app/hooks/api/appConnections/render";
|
||||
import { TRenderSync } from "@app/hooks/api/secretSyncs/types/render-sync";
|
||||
import {
|
||||
useRenderConnectionListEnvironmentGroups,
|
||||
useRenderConnectionListServices
|
||||
} from "@app/hooks/api/appConnections/render";
|
||||
import { RenderSyncScope, TRenderSync } from "@app/hooks/api/secretSyncs/types/render-sync";
|
||||
|
||||
import { getSecretSyncDestinationColValues } from "../helpers";
|
||||
import { SecretSyncTableCell } from "../SecretSyncTableCell";
|
||||
@@ -9,21 +12,59 @@ type Props = {
|
||||
};
|
||||
|
||||
export const RenderSyncDestinationCol = ({ secretSync }: Props) => {
|
||||
const isServiceScope = secretSync.destinationConfig.scope === RenderSyncScope.Service;
|
||||
|
||||
const { data: services = [], isPending } = useRenderConnectionListServices(
|
||||
secretSync.connectionId
|
||||
secretSync.connectionId,
|
||||
{
|
||||
enabled: isServiceScope
|
||||
}
|
||||
);
|
||||
|
||||
const { primaryText, secondaryText } = getSecretSyncDestinationColValues({
|
||||
...secretSync,
|
||||
destinationConfig: {
|
||||
...secretSync.destinationConfig,
|
||||
serviceName: services.find((s) => s.id === secretSync.destinationConfig.serviceId)?.name
|
||||
const { data: groups = [], isPending: isGroupsPending } =
|
||||
useRenderConnectionListEnvironmentGroups(secretSync.connectionId, { enabled: !isServiceScope });
|
||||
|
||||
switch (secretSync.destinationConfig.scope) {
|
||||
case RenderSyncScope.Service: {
|
||||
const id = secretSync.destinationConfig.serviceId;
|
||||
const { primaryText, secondaryText } = getSecretSyncDestinationColValues({
|
||||
...secretSync,
|
||||
destinationConfig: {
|
||||
...secretSync.destinationConfig,
|
||||
serviceName: services.find((s) => s.id === id)?.name
|
||||
}
|
||||
});
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
<SecretSyncTableCell primaryText="Loading service info..." secondaryText="Service" />
|
||||
);
|
||||
}
|
||||
|
||||
return <SecretSyncTableCell primaryText={primaryText} secondaryText={secondaryText} />;
|
||||
}
|
||||
});
|
||||
case RenderSyncScope.EnvironmentGroup: {
|
||||
const id = secretSync.destinationConfig.environmentGroupId;
|
||||
const { primaryText, secondaryText } = getSecretSyncDestinationColValues({
|
||||
...secretSync,
|
||||
destinationConfig: {
|
||||
...secretSync.destinationConfig,
|
||||
environmentGroupName: groups.find((s) => s.id === id)?.name
|
||||
}
|
||||
});
|
||||
|
||||
if (isPending) {
|
||||
return <SecretSyncTableCell primaryText="Loading service info..." secondaryText="Service" />;
|
||||
if (isGroupsPending) {
|
||||
return (
|
||||
<SecretSyncTableCell
|
||||
primaryText="Loading environment group info..."
|
||||
secondaryText="Environment Group"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <SecretSyncTableCell primaryText={primaryText} secondaryText={secondaryText} />;
|
||||
}
|
||||
default:
|
||||
throw new Error("Unknown render sync destination scope");
|
||||
}
|
||||
|
||||
return <SecretSyncTableCell primaryText={primaryText} secondaryText={secondaryText} />;
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from "@app/hooks/api/secretSyncs/types/github-sync";
|
||||
import { GitLabSyncScope } from "@app/hooks/api/secretSyncs/types/gitlab-sync";
|
||||
import { HumanitecSyncScope } from "@app/hooks/api/secretSyncs/types/humanitec-sync";
|
||||
import { RenderSyncScope } from "@app/hooks/api/secretSyncs/types/render-sync";
|
||||
|
||||
// This functional ensures parity across what is displayed in the destination column
|
||||
// and the values used when search filtering
|
||||
@@ -125,8 +126,15 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => {
|
||||
secondaryText = destinationConfig.app;
|
||||
break;
|
||||
case SecretSync.Render:
|
||||
primaryText = destinationConfig.serviceName ?? destinationConfig.serviceId;
|
||||
secondaryText = "Service";
|
||||
if (destinationConfig.scope === RenderSyncScope.Service) {
|
||||
primaryText = destinationConfig.serviceName ?? destinationConfig.serviceId;
|
||||
secondaryText = "Service";
|
||||
} else {
|
||||
primaryText =
|
||||
destinationConfig.environmentGroupName ?? destinationConfig.environmentGroupId;
|
||||
secondaryText = "Environment Group";
|
||||
}
|
||||
|
||||
break;
|
||||
case SecretSync.Flyio:
|
||||
primaryText = destinationConfig.appId;
|
||||
|
||||
@@ -1,23 +1,50 @@
|
||||
import { GenericFieldLabel } from "@app/components/secret-syncs";
|
||||
import { useRenderConnectionListServices } from "@app/hooks/api/appConnections/render";
|
||||
import { TRenderSync } from "@app/hooks/api/secretSyncs/types/render-sync";
|
||||
import {
|
||||
useRenderConnectionListEnvironmentGroups,
|
||||
useRenderConnectionListServices
|
||||
} from "@app/hooks/api/appConnections/render";
|
||||
import { RenderSyncScope, TRenderSync } from "@app/hooks/api/secretSyncs/types/render-sync";
|
||||
|
||||
type Props = {
|
||||
secretSync: TRenderSync;
|
||||
};
|
||||
|
||||
export const RenderSyncDestinationSection = ({ secretSync }: Props) => {
|
||||
const isServiceScope = secretSync.destinationConfig.scope === RenderSyncScope.Service;
|
||||
|
||||
const { data: services = [], isPending } = useRenderConnectionListServices(
|
||||
secretSync.connectionId
|
||||
secretSync.connectionId,
|
||||
{
|
||||
enabled: isServiceScope
|
||||
}
|
||||
);
|
||||
const {
|
||||
destinationConfig: { serviceId }
|
||||
} = secretSync;
|
||||
|
||||
if (isPending) {
|
||||
return <GenericFieldLabel label="Service">Loading...</GenericFieldLabel>;
|
||||
const { data: groups = [], isPending: isGroupsPending } =
|
||||
useRenderConnectionListEnvironmentGroups(secretSync.connectionId, { enabled: !isServiceScope });
|
||||
|
||||
switch (secretSync.destinationConfig.scope) {
|
||||
case RenderSyncScope.Service: {
|
||||
const id = secretSync.destinationConfig.serviceId;
|
||||
|
||||
if (isPending) {
|
||||
return <GenericFieldLabel label="Service">Loading...</GenericFieldLabel>;
|
||||
}
|
||||
|
||||
const serviceName = services.find((service) => service.id === id)?.name;
|
||||
return <GenericFieldLabel label="Service">{serviceName ?? id}</GenericFieldLabel>;
|
||||
}
|
||||
|
||||
case RenderSyncScope.EnvironmentGroup: {
|
||||
const id = secretSync.destinationConfig.environmentGroupId;
|
||||
|
||||
if (isGroupsPending) {
|
||||
return <GenericFieldLabel label="Environment Group">Loading...</GenericFieldLabel>;
|
||||
}
|
||||
|
||||
const envName = groups.find((g) => g.id === id)?.name;
|
||||
return <GenericFieldLabel label="Environment Group">{envName ?? id}</GenericFieldLabel>;
|
||||
}
|
||||
default:
|
||||
throw new Error("Unknown render sync destination scope");
|
||||
}
|
||||
|
||||
const serviceName = services.find((service) => service.id === serviceId)?.name;
|
||||
return <GenericFieldLabel label="Service">{serviceName ?? serviceId}</GenericFieldLabel>;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user