diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 0cc272f15..884f4e9b5 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -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." }, diff --git a/backend/src/server/routes/v1/app-connection-routers/render-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/render-connection-router.ts index 97cff2cd3..bd0202371 100644 --- a/backend/src/server/routes/v1/app-connection-routers/render-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/render-connection-router.ts @@ -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; + } + }); }; diff --git a/backend/src/services/app-connection/render/render-connection-fns.ts b/backend/src/services/app-connection/render/render-connection-fns.ts index bf85b9b71..c41c46c50 100644 --- a/backend/src/services/app-connection/render/render-connection-fns.ts +++ b/backend/src/services/app-connection/render/render-connection-fns.ts @@ -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(`${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 => { + 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(`${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; +}; diff --git a/backend/src/services/app-connection/render/render-connection-service.ts b/backend/src/services/app-connection/render/render-connection-service.ts index 371790bcb..7cf805e9c 100644 --- a/backend/src/services/app-connection/render/render-connection-service.ts +++ b/backend/src/services/app-connection/render/render-connection-service.ts @@ -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 }; }; diff --git a/backend/src/services/app-connection/render/render-connection-types.ts b/backend/src/services/app-connection/render/render-connection-types.ts index 0902472e5..c3922a071 100644 --- a/backend/src/services/app-connection/render/render-connection-types.ts +++ b/backend/src/services/app-connection/render/render-connection-types.ts @@ -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; + }; +}; diff --git a/backend/src/services/secret-sync/render/render-sync-enums.ts b/backend/src/services/secret-sync/render/render-sync-enums.ts index dc0af3b91..098f90de0 100644 --- a/backend/src/services/secret-sync/render/render-sync-enums.ts +++ b/backend/src/services/secret-sync/render/render-sync-enums.ts @@ -1,5 +1,6 @@ export enum RenderSyncScope { - Service = "service" + Service = "service", + EnvironmentGroup = "environment-group" } export enum RenderSyncType { diff --git a/backend/src/services/secret-sync/render/render-sync-fns.ts b/backend/src/services/secret-sync/render/render-sync-fns.ts index 71347f998..4ab67bdd9 100644 --- a/backend/src/services/secret-sync/render/render-sync-fns.ts +++ b/backend/src/services/secret-sync/render/render-sync-fns.ts @@ -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 (requestFn: () => Promise, 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 => { 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 => { + 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); diff --git a/backend/src/services/secret-sync/render/render-sync-schemas.ts b/backend/src/services/secret-sync/render/render-sync-schemas.ts index 0d6e93987..0bc116255 100644 --- a/backend/src/services/secret-sync/render/render-sync-schemas.ts +++ b/backend/src/services/secret-sync/render/render-sync-schemas.ts @@ -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) }) ]); diff --git a/docs/integrations/secret-syncs/render.mdx b/docs/integrations/secret-syncs/render.mdx index 341d84ad5..c81048668 100644 --- a/docs/integrations/secret-syncs/render.mdx +++ b/docs/integrations/secret-syncs/render.mdx @@ -30,8 +30,9 @@ description: "Learn how to configure a Render Sync for Infisical." ![Configure Destination](/images/secret-syncs/render/render-sync-destination.png) - **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**. ![Configure Options](/images/secret-syncs/render/render-sync-options.png) diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/RenderSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/RenderSyncFields.tsx index 3d3f8df93..b88e0d6e6 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/RenderSyncFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/RenderSyncFields.tsx @@ -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 ( <> { setValue("destinationConfig.serviceId", ""); + setValue("destinationConfig.environmentGroupId", ""); setValue("destinationConfig.type", RenderSyncType.Env); setValue("destinationConfig.scope", RenderSyncScope.Service); }} @@ -83,30 +92,67 @@ export const RenderSyncFields = () => { )} /> - ( - - service.id === value) ?? []) : []} - onChange={(option) => { - onChange((option as SingleValue)?.id ?? null); - setValue( - "destinationConfig.serviceName", - (option as SingleValue)?.name ?? "" - ); - }} - options={services} - placeholder="Select a service..." - getOptionLabel={(option) => option.name} - getOptionValue={(option) => option.id.toString()} - /> - - )} - /> + {selectedScope === RenderSyncScope.Service && ( + ( + + service.id === value) ?? []) : []} + onChange={(option) => { + onChange((option as SingleValue)?.id ?? null); + setValue( + "destinationConfig.serviceName", + (option as SingleValue)?.name ?? "" + ); + }} + options={services} + placeholder="Select a service..." + getOptionLabel={(option) => option.name} + getOptionValue={(option) => option.id.toString()} + /> + + )} + /> + )} + + {selectedScope === RenderSyncScope.EnvironmentGroup && ( + ( + + g.id === value) ?? []) : []} + onChange={(option) => { + onChange((option as SingleValue)?.id ?? null); + setValue( + "destinationConfig.environmentGroupName", + (option as SingleValue)?.name ?? "" + ); + }} + options={groups} + placeholder="Select an environment group..." + getOptionLabel={(option) => option.name} + getOptionValue={(option) => option.id.toString()} + /> + + )} + /> + )} ); }; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/RenderSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/RenderSyncReviewFields.tsx index 15f5b6625..f195f0e72 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/RenderSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/RenderSyncReviewFields.tsx @@ -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(); @@ -27,13 +28,20 @@ export const RenderSyncOptionsReviewFields = () => { export const RenderSyncReviewFields = () => { const { watch } = useFormContext(); - const serviceName = watch("destinationConfig.serviceName"); - const scope = watch("destinationConfig.scope"); + const config = watch("destinationConfig"); return ( <> - {scope} - {serviceName} + {config.scope} + {config.scope === RenderSyncScope.Service ? ( + + {config.serviceName ?? config.serviceId} + + ) : ( + + {config.environmentGroupName ?? config.environmentGroupId} + + )} ); }; diff --git a/frontend/src/components/secret-syncs/forms/schemas/render-sync-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/render-sync-destination-schema.ts index 83e5347eb..b65705c14 100644 --- a/frontend/src/components/secret-syncs/forms/schemas/render-sync-destination-schema.ts +++ b/frontend/src/components/secret-syncs/forms/schemas/render-sync-destination-schema.ts @@ -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) }) ]) }) diff --git a/frontend/src/helpers/secretSyncs.ts b/frontend/src/helpers/secretSyncs.ts index 8da61583e..6ce82cfb2 100644 --- a/frontend/src/helpers/secretSyncs.ts +++ b/frontend/src/helpers/secretSyncs.ts @@ -212,5 +212,9 @@ export const RENDER_SYNC_SCOPES: Record - [...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 + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: renderConnectionKeys.listEnvironmentGroups(connectionId), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/app-connections/render/${connectionId}/environment-groups` + ); + + return data; + }, + ...options + }); +}; diff --git a/frontend/src/hooks/api/appConnections/render/types.ts b/frontend/src/hooks/api/appConnections/render/types.ts index ec51adb78..d422a7ae5 100644 --- a/frontend/src/hooks/api/appConnections/render/types.ts +++ b/frontend/src/hooks/api/appConnections/render/types.ts @@ -2,3 +2,8 @@ export type TRenderService = { id: string; name: string; }; + +export type TRenderEnvironmentGroup = { + id: string; + name: string; +}; diff --git a/frontend/src/hooks/api/secretSyncs/types/render-sync.ts b/frontend/src/hooks/api/secretSyncs/types/render-sync.ts index 3d66de623..4d217e840 100644 --- a/frontend/src/hooks/api/secretSyncs/types/render-sync.ts +++ b/frontend/src/hooks/api/secretSyncs/types/render-sync.ts @@ -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 { diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/RenderSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/RenderSyncDestinationCol.tsx index 00fcfe264..ab717f9b0 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/RenderSyncDestinationCol.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/RenderSyncDestinationCol.tsx @@ -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 ( + + ); + } + + return ; } - }); + 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 ; + if (isGroupsPending) { + return ( + + ); + } + + return ; + } + default: + throw new Error("Unknown render sync destination scope"); } - - return ; }; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts index 9b88ba447..01c4024d7 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts @@ -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; diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/RenderSyncDestinationSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/RenderSyncDestinationSection.tsx index eda37a9cf..a9dcab11c 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/RenderSyncDestinationSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/RenderSyncDestinationSection.tsx @@ -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 Loading...; + 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 Loading...; + } + + const serviceName = services.find((service) => service.id === id)?.name; + return {serviceName ?? id}; + } + + case RenderSyncScope.EnvironmentGroup: { + const id = secretSync.destinationConfig.environmentGroupId; + + if (isGroupsPending) { + return Loading...; + } + + const envName = groups.find((g) => g.id === id)?.name; + return {envName ?? id}; + } + default: + throw new Error("Unknown render sync destination scope"); } - - const serviceName = services.find((service) => service.id === serviceId)?.name; - return {serviceName ?? serviceId}; };