diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 7c79cb54e..c18c87c39 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -2390,6 +2390,11 @@ export const SecretSyncs = { ONEPASS: { vaultId: "The ID of the 1Password vault to sync secrets to." }, + RENDER: { + serviceId: "The ID of the Render service to sync secrets to.", + scope: "The Render scope that secrets should be synced to.", + type: "The Render resource type to sync secrets to." + }, FLYIO: { appId: "The ID of the Fly.io app to sync secrets to." } diff --git a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts index 3a9af21c7..ab84c014d 100644 --- a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts @@ -61,6 +61,10 @@ import { PostgresConnectionListItemSchema, SanitizedPostgresConnectionSchema } from "@app/services/app-connection/postgres"; +import { + RenderConnectionListItemSchema, + SanitizedRenderConnectionSchema +} from "@app/services/app-connection/render/render-connection-schema"; import { SanitizedTeamCityConnectionSchema, TeamCityConnectionListItemSchema @@ -102,6 +106,7 @@ const SanitizedAppConnectionSchema = z.union([ ...SanitizedOCIConnectionSchema.options, ...SanitizedOracleDBConnectionSchema.options, ...SanitizedOnePassConnectionSchema.options, + ...SanitizedRenderConnectionSchema.options, ...SanitizedFlyioConnectionSchema.options ]); @@ -130,6 +135,7 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ OCIConnectionListItemSchema, OracleDBConnectionListItemSchema, OnePassConnectionListItemSchema, + RenderConnectionListItemSchema, FlyioConnectionListItemSchema ]); diff --git a/backend/src/server/routes/v1/app-connection-routers/index.ts b/backend/src/server/routes/v1/app-connection-routers/index.ts index 7c8df632c..45af934a7 100644 --- a/backend/src/server/routes/v1/app-connection-routers/index.ts +++ b/backend/src/server/routes/v1/app-connection-routers/index.ts @@ -21,6 +21,7 @@ import { registerLdapConnectionRouter } from "./ldap-connection-router"; import { registerMsSqlConnectionRouter } from "./mssql-connection-router"; import { registerMySqlConnectionRouter } from "./mysql-connection-router"; import { registerPostgresConnectionRouter } from "./postgres-connection-router"; +import { registerRenderConnectionRouter } from "./render-connection-router"; import { registerTeamCityConnectionRouter } from "./teamcity-connection-router"; import { registerTerraformCloudConnectionRouter } from "./terraform-cloud-router"; import { registerVercelConnectionRouter } from "./vercel-connection-router"; @@ -54,5 +55,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record { + registerAppConnectionEndpoints({ + app: AppConnection.Render, + server, + sanitizedResponseSchema: SanitizedRenderConnectionSchema, + createSchema: CreateRenderConnectionSchema, + updateSchema: UpdateRenderConnectionSchema + }); + + // The below endpoints are not exposed and for Infisical App use + server.route({ + method: "GET", + url: `/:connectionId/services`, + 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 services = await server.services.appConnection.render.listServices(connectionId, req.permission); + + return services; + } + }); +}; diff --git a/backend/src/server/routes/v1/secret-sync-routers/index.ts b/backend/src/server/routes/v1/secret-sync-routers/index.ts index 39d6747a4..675b74982 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/index.ts +++ b/backend/src/server/routes/v1/secret-sync-routers/index.ts @@ -14,6 +14,7 @@ import { registerGcpSyncRouter } from "./gcp-sync-router"; import { registerGitHubSyncRouter } from "./github-sync-router"; import { registerHCVaultSyncRouter } from "./hc-vault-sync-router"; import { registerHumanitecSyncRouter } from "./humanitec-sync-router"; +import { registerRenderSyncRouter } from "./render-sync-router"; import { registerTeamCitySyncRouter } from "./teamcity-sync-router"; import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router"; import { registerVercelSyncRouter } from "./vercel-sync-router"; @@ -39,5 +40,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record + registerSyncSecretsEndpoints({ + destination: SecretSync.Render, + server, + responseSchema: RenderSyncSchema, + createSchema: CreateRenderSyncSchema, + updateSchema: UpdateRenderSyncSchema + }); diff --git a/backend/src/server/routes/v1/secret-sync-routers/secret-sync-router.ts b/backend/src/server/routes/v1/secret-sync-routers/secret-sync-router.ts index 7647a1c94..908d332d3 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/secret-sync-router.ts +++ b/backend/src/server/routes/v1/secret-sync-routers/secret-sync-router.ts @@ -28,6 +28,7 @@ import { GcpSyncListItemSchema, GcpSyncSchema } from "@app/services/secret-sync/ import { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret-sync/github"; import { HCVaultSyncListItemSchema, HCVaultSyncSchema } from "@app/services/secret-sync/hc-vault"; import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec"; +import { RenderSyncListItemSchema, RenderSyncSchema } from "@app/services/secret-sync/render/render-sync-schemas"; import { TeamCitySyncListItemSchema, TeamCitySyncSchema } from "@app/services/secret-sync/teamcity"; import { TerraformCloudSyncListItemSchema, TerraformCloudSyncSchema } from "@app/services/secret-sync/terraform-cloud"; import { VercelSyncListItemSchema, VercelSyncSchema } from "@app/services/secret-sync/vercel"; @@ -51,6 +52,7 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [ TeamCitySyncSchema, OCIVaultSyncSchema, OnePassSyncSchema, + RenderSyncSchema, FlyioSyncSchema ]); @@ -72,6 +74,7 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [ TeamCitySyncListItemSchema, OCIVaultSyncListItemSchema, OnePassSyncListItemSchema, + RenderSyncListItemSchema, FlyioSyncListItemSchema ]); diff --git a/backend/src/services/app-connection/app-connection-enums.ts b/backend/src/services/app-connection/app-connection-enums.ts index d7c03fd3f..7d210897d 100644 --- a/backend/src/services/app-connection/app-connection-enums.ts +++ b/backend/src/services/app-connection/app-connection-enums.ts @@ -23,6 +23,7 @@ export enum AppConnection { OCI = "oci", OracleDB = "oracledb", OnePass = "1password", + Render = "render", Flyio = "flyio" } diff --git a/backend/src/services/app-connection/app-connection-fns.ts b/backend/src/services/app-connection/app-connection-fns.ts index 1d9bf0be9..4c97b0392 100644 --- a/backend/src/services/app-connection/app-connection-fns.ts +++ b/backend/src/services/app-connection/app-connection-fns.ts @@ -79,6 +79,8 @@ import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql"; import { MySqlConnectionMethod } from "./mysql/mysql-connection-enums"; import { getMySqlConnectionListItem } from "./mysql/mysql-connection-fns"; import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres"; +import { RenderConnectionMethod } from "./render/render-connection-enums"; +import { getRenderConnectionListItem, validateRenderConnectionCredentials } from "./render/render-connection-fns"; import { getTeamCityConnectionListItem, TeamCityConnectionMethod, @@ -123,6 +125,7 @@ export const listAppConnectionOptions = () => { getOCIConnectionListItem(), getOracleDBConnectionListItem(), getOnePassConnectionListItem(), + getRenderConnectionListItem(), getFlyioConnectionListItem() ].sort((a, b) => a.name.localeCompare(b.name)); }; @@ -199,6 +202,7 @@ export const validateAppConnectionCredentials = async ( [AppConnection.OCI]: validateOCIConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.OracleDB]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.OnePass]: validateOnePassConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.Render]: validateRenderConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Flyio]: validateFlyioConnectionCredentials as TAppConnectionCredentialsValidator }; @@ -249,6 +253,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) => return "App Role"; case LdapConnectionMethod.SimpleBind: return "Simple Bind"; + case RenderConnectionMethod.ApiKey: + return "API Key"; default: // eslint-disable-next-line @typescript-eslint/restrict-template-expressions throw new Error(`Unhandled App Connection Method: ${method}`); @@ -304,6 +310,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record< [AppConnection.OCI]: platformManagedCredentialsNotSupported, [AppConnection.OracleDB]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform, [AppConnection.OnePass]: platformManagedCredentialsNotSupported, + [AppConnection.Render]: platformManagedCredentialsNotSupported, [AppConnection.Flyio]: platformManagedCredentialsNotSupported }; diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts index 7080d7053..24dc31f99 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -25,6 +25,7 @@ export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.OCI]: "OCI", [AppConnection.OracleDB]: "OracleDB", [AppConnection.OnePass]: "1Password", + [AppConnection.Render]: "Render", [AppConnection.Flyio]: "Fly.io" }; @@ -53,5 +54,6 @@ export const APP_CONNECTION_PLAN_MAP: Record { + return { + name: "Render" as const, + app: AppConnection.Render as const, + methods: Object.values(RenderConnectionMethod) as [RenderConnectionMethod.ApiKey] + }; +}; + +export const listRenderServices = async (appConnection: TRenderConnection): Promise => { + const { + credentials: { apiKey } + } = appConnection; + + const services: TRenderService[] = []; + let hasMorePages = true; + const perPage = 100; + let cursor; + + while (hasMorePages) { + const res: TRawRenderService[] = ( + await request.get(`${IntegrationUrls.RENDER_API_URL}/v1/services`, { + 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) => { + services.push({ + name: item.service.name, + id: item.service.id + }); + }); + + if (res.length < perPage) { + hasMorePages = false; + } else { + cursor = res[res.length - 1].cursor; + } + } + + return services; +}; + +export const validateRenderConnectionCredentials = async (config: TRenderConnectionConfig) => { + const { credentials: inputCredentials } = config; + + try { + await request.get(`${IntegrationUrls.RENDER_API_URL}/v1/users`, { + headers: { + Authorization: `Bearer ${inputCredentials.apiKey}` + } + }); + } catch (error: unknown) { + if (error instanceof AxiosError) { + throw new BadRequestError({ + message: `Failed to validate credentials: ${error.message || "Unknown error"}` + }); + } + throw new BadRequestError({ + message: "Unable to validate connection: verify credentials" + }); + } + + return inputCredentials; +}; diff --git a/backend/src/services/app-connection/render/render-connection-schema.ts b/backend/src/services/app-connection/render/render-connection-schema.ts new file mode 100644 index 000000000..77cc46714 --- /dev/null +++ b/backend/src/services/app-connection/render/render-connection-schema.ts @@ -0,0 +1,56 @@ +import z from "zod"; + +import { AppConnections } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + BaseAppConnectionSchema, + GenericCreateAppConnectionFieldsSchema, + GenericUpdateAppConnectionFieldsSchema +} from "@app/services/app-connection/app-connection-schemas"; + +import { RenderConnectionMethod } from "./render-connection-enums"; + +export const RenderConnectionApiKeyCredentialsSchema = z.object({ + apiKey: z.string().trim().min(1, "API key required").max(256, "API key cannot exceed 256 characters") +}); + +const BaseRenderConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Render) }); + +export const RenderConnectionSchema = BaseRenderConnectionSchema.extend({ + method: z.literal(RenderConnectionMethod.ApiKey), + credentials: RenderConnectionApiKeyCredentialsSchema +}); + +export const SanitizedRenderConnectionSchema = z.discriminatedUnion("method", [ + BaseRenderConnectionSchema.extend({ + method: z.literal(RenderConnectionMethod.ApiKey), + credentials: RenderConnectionApiKeyCredentialsSchema.pick({}) + }) +]); + +export const ValidateRenderConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: z.literal(RenderConnectionMethod.ApiKey).describe(AppConnections.CREATE(AppConnection.Render).method), + credentials: RenderConnectionApiKeyCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.Render).credentials + ) + }) +]); + +export const CreateRenderConnectionSchema = ValidateRenderConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.Render) +); + +export const UpdateRenderConnectionSchema = z + .object({ + credentials: RenderConnectionApiKeyCredentialsSchema.optional().describe( + AppConnections.UPDATE(AppConnection.Render).credentials + ) + }) + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Render)); + +export const RenderConnectionListItemSchema = z.object({ + name: z.literal("Render"), + app: z.literal(AppConnection.Render), + methods: z.nativeEnum(RenderConnectionMethod).array() +}); diff --git a/backend/src/services/app-connection/render/render-connection-service.ts b/backend/src/services/app-connection/render/render-connection-service.ts new file mode 100644 index 000000000..371790bcb --- /dev/null +++ b/backend/src/services/app-connection/render/render-connection-service.ts @@ -0,0 +1,30 @@ +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 { TRenderConnection } from "./render-connection-types"; + +type TGetAppConnectionFunc = ( + app: AppConnection, + connectionId: string, + actor: OrgServiceActor +) => Promise; + +export const renderConnectionService = (getAppConnection: TGetAppConnectionFunc) => { + const listServices = async (connectionId: string, actor: OrgServiceActor) => { + const appConnection = await getAppConnection(AppConnection.Render, connectionId, actor); + try { + const services = await listRenderServices(appConnection); + + return services; + } catch (error) { + logger.error(error, "Failed to list services for Render connection"); + return []; + } + }; + + return { + listServices + }; +}; diff --git a/backend/src/services/app-connection/render/render-connection-types.ts b/backend/src/services/app-connection/render/render-connection-types.ts new file mode 100644 index 000000000..0902472e5 --- /dev/null +++ b/backend/src/services/app-connection/render/render-connection-types.ts @@ -0,0 +1,35 @@ +import z from "zod"; + +import { DiscriminativePick } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { + CreateRenderConnectionSchema, + RenderConnectionSchema, + ValidateRenderConnectionCredentialsSchema +} from "./render-connection-schema"; + +export type TRenderConnection = z.infer; + +export type TRenderConnectionInput = z.infer & { + app: AppConnection.Render; +}; + +export type TValidateRenderConnectionCredentialsSchema = typeof ValidateRenderConnectionCredentialsSchema; + +export type TRenderConnectionConfig = DiscriminativePick & { + orgId: string; +}; + +export type TRenderService = { + name: string; + id: string; +}; + +export type TRawRenderService = { + cursor: string; + service: { + id: string; + name: string; + }; +}; diff --git a/backend/src/services/secret-sync/render/index.ts b/backend/src/services/secret-sync/render/index.ts new file mode 100644 index 000000000..7e8ddd412 --- /dev/null +++ b/backend/src/services/secret-sync/render/index.ts @@ -0,0 +1,4 @@ +export * from "./render-sync-constants"; +export * from "./render-sync-fns"; +export * from "./render-sync-schemas"; +export * from "./render-sync-types"; diff --git a/backend/src/services/secret-sync/render/render-sync-constants.ts b/backend/src/services/secret-sync/render/render-sync-constants.ts new file mode 100644 index 000000000..0246a95d9 --- /dev/null +++ b/backend/src/services/secret-sync/render/render-sync-constants.ts @@ -0,0 +1,10 @@ +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types"; + +export const RENDER_SYNC_LIST_OPTION: TSecretSyncListItem = { + name: "Render", + destination: SecretSync.Render, + connection: AppConnection.Render, + canImportSecrets: true +}; diff --git a/backend/src/services/secret-sync/render/render-sync-enums.ts b/backend/src/services/secret-sync/render/render-sync-enums.ts new file mode 100644 index 000000000..dc0af3b91 --- /dev/null +++ b/backend/src/services/secret-sync/render/render-sync-enums.ts @@ -0,0 +1,8 @@ +export enum RenderSyncScope { + Service = "service" +} + +export enum RenderSyncType { + Env = "env", + File = "file" +} diff --git a/backend/src/services/secret-sync/render/render-sync-fns.ts b/backend/src/services/secret-sync/render/render-sync-fns.ts new file mode 100644 index 000000000..8a9039e2e --- /dev/null +++ b/backend/src/services/secret-sync/render/render-sync-fns.ts @@ -0,0 +1,134 @@ +/* eslint-disable no-await-in-loop */ +import { request } from "@app/lib/config/request"; +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 { TRenderSecret, TRenderSyncWithCredentials } from "./render-sync-types"; + +const getRenderEnvironmentSecrets = async (secretSync: TRenderSyncWithCredentials) => { + const { + destinationConfig, + connection: { + credentials: { apiKey } + } + } = secretSync; + + const baseUrl = `${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/env-vars`; + const allSecrets: TRenderSecret[] = []; + let cursor: string | undefined; + + do { + const url = cursor ? `${baseUrl}?cursor=${cursor}` : baseUrl; + const { data } = await 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); + + cursor = data[data.length - 1]?.cursor; + } while (cursor); + + return allSecrets; +}; + +const putEnvironmentSecret = async (secretSync: TRenderSyncWithCredentials, secretMap: TSecretMap, key: string) => { + const { + destinationConfig, + connection: { + credentials: { apiKey } + } + } = secretSync; + + await request.put( + `${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/env-vars/${key}`, + { + key, + value: secretMap[key].value + }, + { + headers: { + Authorization: `Bearer ${apiKey}`, + Accept: "application/json" + } + } + ); +}; + +const deleteEnvironmentSecret = async (secretSync: TRenderSyncWithCredentials, secret: TRenderSecret) => { + const { + destinationConfig, + connection: { + credentials: { apiKey } + } + } = secretSync; + + await request.delete( + `${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/env-vars/${secret.key}`, + { + headers: { + Authorization: `Bearer ${apiKey}`, + Accept: "application/json" + } + } + ); +}; + +const sleep = async () => + new Promise((resolve) => { + setTimeout(resolve, 500); + }); + +export const RenderSyncFns = { + syncSecrets: async (secretSync: TRenderSyncWithCredentials, secretMap: TSecretMap) => { + const renderSecrets = await getRenderEnvironmentSecrets(secretSync); + for await (const key of Object.keys(secretMap)) { + await putEnvironmentSecret(secretSync, secretMap, key); + await sleep(); + } + + if (secretSync.syncOptions.disableSecretDeletion) return; + + 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 => { + const renderSecrets = await getRenderEnvironmentSecrets(secretSync); + return Object.fromEntries(renderSecrets.map((secret) => [secret.key, { value: secret.value ?? "" }])); + }, + + removeSecrets: async (secretSync: TRenderSyncWithCredentials, secretMap: TSecretMap) => { + const encryptedSecrets = await getRenderEnvironmentSecrets(secretSync); + + for await (const encryptedSecret of encryptedSecrets) { + if (encryptedSecret.key in secretMap) { + await deleteEnvironmentSecret(secretSync, encryptedSecret); + await sleep(); + } + } + } +}; diff --git a/backend/src/services/secret-sync/render/render-sync-schemas.ts b/backend/src/services/secret-sync/render/render-sync-schemas.ts new file mode 100644 index 000000000..77414c17c --- /dev/null +++ b/backend/src/services/secret-sync/render/render-sync-schemas.ts @@ -0,0 +1,49 @@ +import { z } from "zod"; + +import { SecretSyncs } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { + BaseSecretSyncSchema, + GenericCreateSecretSyncFieldsSchema, + GenericUpdateSecretSyncFieldsSchema +} from "@app/services/secret-sync/secret-sync-schemas"; +import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; + +import { RenderSyncScope, RenderSyncType } from "./render-sync-enums"; + +const RenderSyncDestinationConfigSchema = z.discriminatedUnion("scope", [ + z.object({ + 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) + }) +]); + +const RenderSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true }; + +export const RenderSyncSchema = BaseSecretSyncSchema(SecretSync.Render, RenderSyncOptionsConfig).extend({ + destination: z.literal(SecretSync.Render), + destinationConfig: RenderSyncDestinationConfigSchema +}); + +export const CreateRenderSyncSchema = GenericCreateSecretSyncFieldsSchema( + SecretSync.Render, + RenderSyncOptionsConfig +).extend({ + destinationConfig: RenderSyncDestinationConfigSchema +}); + +export const UpdateRenderSyncSchema = GenericUpdateSecretSyncFieldsSchema( + SecretSync.Render, + RenderSyncOptionsConfig +).extend({ + destinationConfig: RenderSyncDestinationConfigSchema.optional() +}); + +export const RenderSyncListItemSchema = z.object({ + name: z.literal("Render"), + connection: z.literal(AppConnection.Render), + destination: z.literal(SecretSync.Render), + canImportSecrets: z.literal(true) +}); diff --git a/backend/src/services/secret-sync/render/render-sync-types.ts b/backend/src/services/secret-sync/render/render-sync-types.ts new file mode 100644 index 000000000..22d479384 --- /dev/null +++ b/backend/src/services/secret-sync/render/render-sync-types.ts @@ -0,0 +1,20 @@ +import z from "zod"; + +import { TRenderConnection } from "@app/services/app-connection/render/render-connection-types"; + +import { CreateRenderSyncSchema, RenderSyncListItemSchema, RenderSyncSchema } from "./render-sync-schemas"; + +export type TRenderSyncListItem = z.infer; + +export type TRenderSync = z.infer; + +export type TRenderSyncInput = z.infer; + +export type TRenderSyncWithCredentials = TRenderSync & { + connection: TRenderConnection; +}; + +export type TRenderSecret = { + key: string; + value: string; +}; diff --git a/backend/src/services/secret-sync/secret-sync-enums.ts b/backend/src/services/secret-sync/secret-sync-enums.ts index 85e458feb..2ac235fe2 100644 --- a/backend/src/services/secret-sync/secret-sync-enums.ts +++ b/backend/src/services/secret-sync/secret-sync-enums.ts @@ -16,6 +16,7 @@ export enum SecretSync { TeamCity = "teamcity", OCIVault = "oci-vault", OnePass = "1password", + Render = "render", Flyio = "flyio" } diff --git a/backend/src/services/secret-sync/secret-sync-fns.ts b/backend/src/services/secret-sync/secret-sync-fns.ts index 30589785e..71c413c11 100644 --- a/backend/src/services/secret-sync/secret-sync-fns.ts +++ b/backend/src/services/secret-sync/secret-sync-fns.ts @@ -35,6 +35,7 @@ import { GcpSyncFns } from "./gcp/gcp-sync-fns"; import { HC_VAULT_SYNC_LIST_OPTION, HCVaultSyncFns } from "./hc-vault"; import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec"; import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns"; +import { RENDER_SYNC_LIST_OPTION, RenderSyncFns } from "./render"; import { SECRET_SYNC_PLAN_MAP } from "./secret-sync-maps"; import { TEAMCITY_SYNC_LIST_OPTION, TeamCitySyncFns } from "./teamcity"; import { TERRAFORM_CLOUD_SYNC_LIST_OPTION, TerraformCloudSyncFns } from "./terraform-cloud"; @@ -59,6 +60,7 @@ const SECRET_SYNC_LIST_OPTIONS: Record = { [SecretSync.TeamCity]: TEAMCITY_SYNC_LIST_OPTION, [SecretSync.OCIVault]: OCI_VAULT_SYNC_LIST_OPTION, [SecretSync.OnePass]: ONEPASS_SYNC_LIST_OPTION, + [SecretSync.Render]: RENDER_SYNC_LIST_OPTION, [SecretSync.Flyio]: FLYIO_SYNC_LIST_OPTION }; @@ -217,6 +219,8 @@ export const SecretSyncFns = { return OCIVaultSyncFns.syncSecrets(secretSync, schemaSecretMap); case SecretSync.OnePass: return OnePassSyncFns.syncSecrets(secretSync, schemaSecretMap); + case SecretSync.Render: + return RenderSyncFns.syncSecrets(secretSync, schemaSecretMap); case SecretSync.Flyio: return FlyioSyncFns.syncSecrets(secretSync, schemaSecretMap); default: @@ -296,6 +300,9 @@ export const SecretSyncFns = { case SecretSync.OnePass: secretMap = await OnePassSyncFns.getSecrets(secretSync); break; + case SecretSync.Render: + secretMap = await RenderSyncFns.getSecrets(secretSync); + break; case SecretSync.Flyio: secretMap = await FlyioSyncFns.getSecrets(secretSync); break; @@ -366,6 +373,8 @@ export const SecretSyncFns = { return OCIVaultSyncFns.removeSecrets(secretSync, schemaSecretMap); case SecretSync.OnePass: return OnePassSyncFns.removeSecrets(secretSync, schemaSecretMap); + case SecretSync.Render: + return RenderSyncFns.removeSecrets(secretSync, schemaSecretMap); case SecretSync.Flyio: return FlyioSyncFns.removeSecrets(secretSync, schemaSecretMap); default: diff --git a/backend/src/services/secret-sync/secret-sync-maps.ts b/backend/src/services/secret-sync/secret-sync-maps.ts index 4e2f50b70..47cd7c164 100644 --- a/backend/src/services/secret-sync/secret-sync-maps.ts +++ b/backend/src/services/secret-sync/secret-sync-maps.ts @@ -19,6 +19,7 @@ export const SECRET_SYNC_NAME_MAP: Record = { [SecretSync.TeamCity]: "TeamCity", [SecretSync.OCIVault]: "OCI Vault", [SecretSync.OnePass]: "1Password", + [SecretSync.Render]: "Render", [SecretSync.Flyio]: "Fly.io" }; @@ -40,6 +41,7 @@ export const SECRET_SYNC_CONNECTION_MAP: Record = { [SecretSync.TeamCity]: AppConnection.TeamCity, [SecretSync.OCIVault]: AppConnection.OCI, [SecretSync.OnePass]: AppConnection.OnePass, + [SecretSync.Render]: AppConnection.Render, [SecretSync.Flyio]: AppConnection.Flyio }; @@ -61,5 +63,6 @@ export const SECRET_SYNC_PLAN_MAP: Record = { [SecretSync.TeamCity]: SecretSyncPlanType.Regular, [SecretSync.OCIVault]: SecretSyncPlanType.Enterprise, [SecretSync.OnePass]: SecretSyncPlanType.Regular, + [SecretSync.Render]: SecretSyncPlanType.Regular, [SecretSync.Flyio]: SecretSyncPlanType.Regular }; diff --git a/backend/src/services/secret-sync/secret-sync-types.ts b/backend/src/services/secret-sync/secret-sync-types.ts index 192140db5..f41d8e27b 100644 --- a/backend/src/services/secret-sync/secret-sync-types.ts +++ b/backend/src/services/secret-sync/secret-sync-types.ts @@ -86,6 +86,12 @@ import { THumanitecSyncListItem, THumanitecSyncWithCredentials } from "./humanitec"; +import { + TRenderSync, + TRenderSyncInput, + TRenderSyncListItem, + TRenderSyncWithCredentials +} from "./render/render-sync-types"; import { TTeamCitySync, TTeamCitySyncInput, @@ -118,6 +124,7 @@ export type TSecretSync = | TTeamCitySync | TOCIVaultSync | TOnePassSync + | TRenderSync | TFlyioSync; export type TSecretSyncWithCredentials = @@ -138,6 +145,7 @@ export type TSecretSyncWithCredentials = | TTeamCitySyncWithCredentials | TOCIVaultSyncWithCredentials | TOnePassSyncWithCredentials + | TRenderSyncWithCredentials | TFlyioSyncWithCredentials; export type TSecretSyncInput = @@ -158,6 +166,7 @@ export type TSecretSyncInput = | TTeamCitySyncInput | TOCIVaultSyncInput | TOnePassSyncInput + | TRenderSyncInput | TFlyioSyncInput; export type TSecretSyncListItem = @@ -178,6 +187,7 @@ export type TSecretSyncListItem = | TTeamCitySyncListItem | TOCIVaultSyncListItem | TOnePassSyncListItem + | TRenderSyncListItem | TFlyioSyncListItem; export type TSyncOptionsConfig = { diff --git a/docs/api-reference/endpoints/app-connections/render/available.mdx b/docs/api-reference/endpoints/app-connections/render/available.mdx new file mode 100644 index 000000000..99691fe59 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/render/available.mdx @@ -0,0 +1,4 @@ +--- +title: "Available" +openapi: "GET /api/v1/app-connections/render/available" +--- diff --git a/docs/api-reference/endpoints/app-connections/render/create.mdx b/docs/api-reference/endpoints/app-connections/render/create.mdx new file mode 100644 index 000000000..2078e0418 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/render/create.mdx @@ -0,0 +1,10 @@ +--- +title: "Create" +openapi: "POST /api/v1/app-connections/render" +--- + + + Check out the configuration docs for [Render + Connections](/integrations/app-connections/render) to learn how to obtain the + required credentials. + diff --git a/docs/api-reference/endpoints/app-connections/render/delete.mdx b/docs/api-reference/endpoints/app-connections/render/delete.mdx new file mode 100644 index 000000000..40826c700 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/render/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/render/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/render/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/render/get-by-id.mdx new file mode 100644 index 000000000..7c4f7ce05 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/render/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/render/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/render/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/render/get-by-name.mdx new file mode 100644 index 000000000..464a36558 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/render/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/render/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/render/list.mdx b/docs/api-reference/endpoints/app-connections/render/list.mdx new file mode 100644 index 000000000..f473057f1 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/render/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/render" +--- diff --git a/docs/api-reference/endpoints/app-connections/render/update.mdx b/docs/api-reference/endpoints/app-connections/render/update.mdx new file mode 100644 index 000000000..9c8e33484 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/render/update.mdx @@ -0,0 +1,10 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/render/{connectionId}" +--- + + + Check out the configuration docs for [Render + Connections](/integrations/app-connections/render) to learn how to obtain the + required credentials. + diff --git a/docs/api-reference/endpoints/secret-syncs/render/create.mdx b/docs/api-reference/endpoints/secret-syncs/render/create.mdx new file mode 100644 index 000000000..2afff8511 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/render/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-syncs/render" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/render/delete.mdx b/docs/api-reference/endpoints/secret-syncs/render/delete.mdx new file mode 100644 index 000000000..eef2dbe61 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/render/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-syncs/render/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/render/get-by-id.mdx b/docs/api-reference/endpoints/secret-syncs/render/get-by-id.mdx new file mode 100644 index 000000000..6918c0645 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/render/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/secret-syncs/render/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/render/get-by-name.mdx b/docs/api-reference/endpoints/secret-syncs/render/get-by-name.mdx new file mode 100644 index 000000000..c9ff4f0ff --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/render/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/secret-syncs/render/sync-name/{syncName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/render/import-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/render/import-secrets.mdx new file mode 100644 index 000000000..1ef9069e8 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/render/import-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Import Secrets" +openapi: "POST /api/v1/secret-syncs/render/{syncId}/import-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/render/list.mdx b/docs/api-reference/endpoints/secret-syncs/render/list.mdx new file mode 100644 index 000000000..82aa6eb88 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/render/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-syncs/render" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/render/remove-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/render/remove-secrets.mdx new file mode 100644 index 000000000..9130316a7 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/render/remove-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Secrets" +openapi: "POST /api/v1/secret-syncs/render/{syncId}/remove-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/render/sync-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/render/sync-secrets.mdx new file mode 100644 index 000000000..a08b99d42 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/render/sync-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Secrets" +openapi: "POST /api/v1/secret-syncs/render/{syncId}/sync-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/render/update.mdx b/docs/api-reference/endpoints/secret-syncs/render/update.mdx new file mode 100644 index 000000000..1041ccc18 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/render/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/secret-syncs/render/{syncId}" +--- diff --git a/docs/images/app-connections/render/render-account-settings.png b/docs/images/app-connections/render/render-account-settings.png new file mode 100644 index 000000000..616fc38a4 Binary files /dev/null and b/docs/images/app-connections/render/render-account-settings.png differ diff --git a/docs/images/app-connections/render/render-app-connection-created.png b/docs/images/app-connections/render/render-app-connection-created.png new file mode 100644 index 000000000..359c19104 Binary files /dev/null and b/docs/images/app-connections/render/render-app-connection-created.png differ diff --git a/docs/images/app-connections/render/render-app-connection-form.png b/docs/images/app-connections/render/render-app-connection-form.png new file mode 100644 index 000000000..90fb296e7 Binary files /dev/null and b/docs/images/app-connections/render/render-app-connection-form.png differ diff --git a/docs/images/app-connections/render/render-app-connection-select.png b/docs/images/app-connections/render/render-app-connection-select.png new file mode 100644 index 000000000..4349c7401 Binary files /dev/null and b/docs/images/app-connections/render/render-app-connection-select.png differ diff --git a/docs/images/app-connections/render/render-create-api-key.png b/docs/images/app-connections/render/render-create-api-key.png new file mode 100644 index 000000000..e6fc7aae9 Binary files /dev/null and b/docs/images/app-connections/render/render-create-api-key.png differ diff --git a/docs/images/app-connections/render/render-name-api-key.png b/docs/images/app-connections/render/render-name-api-key.png new file mode 100644 index 000000000..85c90e721 Binary files /dev/null and b/docs/images/app-connections/render/render-name-api-key.png differ diff --git a/docs/images/secret-syncs/render/render-sync-created.png b/docs/images/secret-syncs/render/render-sync-created.png new file mode 100644 index 000000000..726cd14f6 Binary files /dev/null and b/docs/images/secret-syncs/render/render-sync-created.png differ diff --git a/docs/images/secret-syncs/render/render-sync-destination.png b/docs/images/secret-syncs/render/render-sync-destination.png new file mode 100644 index 000000000..67fd1af85 Binary files /dev/null and b/docs/images/secret-syncs/render/render-sync-destination.png differ diff --git a/docs/images/secret-syncs/render/render-sync-details.png b/docs/images/secret-syncs/render/render-sync-details.png new file mode 100644 index 000000000..ff50b26cd Binary files /dev/null and b/docs/images/secret-syncs/render/render-sync-details.png differ diff --git a/docs/images/secret-syncs/render/render-sync-options.png b/docs/images/secret-syncs/render/render-sync-options.png new file mode 100644 index 000000000..25cc9dad5 Binary files /dev/null and b/docs/images/secret-syncs/render/render-sync-options.png differ diff --git a/docs/images/secret-syncs/render/render-sync-review.png b/docs/images/secret-syncs/render/render-sync-review.png new file mode 100644 index 000000000..ab39faf20 Binary files /dev/null and b/docs/images/secret-syncs/render/render-sync-review.png differ diff --git a/docs/images/secret-syncs/render/render-sync-source.png b/docs/images/secret-syncs/render/render-sync-source.png new file mode 100644 index 000000000..436e1d621 Binary files /dev/null and b/docs/images/secret-syncs/render/render-sync-source.png differ diff --git a/docs/images/secret-syncs/render/select-render-option.png b/docs/images/secret-syncs/render/select-render-option.png new file mode 100644 index 000000000..6e47be640 Binary files /dev/null and b/docs/images/secret-syncs/render/select-render-option.png differ diff --git a/docs/integrations/app-connections/render.mdx b/docs/integrations/app-connections/render.mdx new file mode 100644 index 000000000..580ec953e --- /dev/null +++ b/docs/integrations/app-connections/render.mdx @@ -0,0 +1,55 @@ +--- +title: "Render Connection" +description: "Learn how to configure a Render Connection for Infisical." +--- + +Infisical supports connecting to Render using API keys for secure access to your Render services. + +## Configure API Key for Infisical + + + + Navigate to your Render dashboard and click on **Account Settings** in the + top right corner. ![Account + Settings](/images/app-connections/render/render-account-settings.png) + + + In the Account Settings page, scroll down to the **API Keys** section and + click **Create API Key**. ![Create API + Key](/images/app-connections/render/render-create-api-key.png) + + + Enter a descriptive name for your API key (e.g., "production") + and click **Create API Key**. ![Name API + Key](/images/app-connections/render/render-name-api-key.png) + + + After creation, you'll be shown your API key. Make sure to copy and securely + store this key as it will not be shown again. + + + +## Setup Render Connection in Infisical + + + + Navigate to the **App Connections** tab on the **Organization Settings** + page. ![App Connections + Tab](/images/app-connections/general/add-connection.png) + + + Select the **Render Connection** option from the connection options modal. + ![Select Render + Connection](/images/app-connections/render/render-app-connection-select.png) + + + Enter your Render API key in the provided field and click **Connect to + Render** to establish the connection. ![Connect to + Render](/images/app-connections/render/render-app-connection-form.png) + + + Your **Render Connection** is now available for use in your Infisical + projects. ![Render Connection + Created](/images/app-connections/render/render-app-connection-created.png) + + diff --git a/docs/integrations/cloud/render.mdx b/docs/integrations/cloud/render.mdx index 366316171..1d4860ebd 100644 --- a/docs/integrations/cloud/render.mdx +++ b/docs/integrations/cloud/render.mdx @@ -3,30 +3,7 @@ title: "Render" description: "How to sync secrets from Infisical to Render" --- -Prerequisites: - -- Set up and add envars to [Infisical Cloud](https://app.infisical.com) - - - - Obtain a Render API Key in your Render Account Settings > API Keys. - - ![integrations render dashboard](../../images/integrations/render/integrations-render-dashboard.png) - ![integrations render token](../../images/integrations/render/integrations-render-token.png) - - Navigate to your project's integrations tab in Infisical. - - ![integrations](../../images/integrations.png) - - Press on the Render tile and input your Render API Key to grant Infisical access to your Render account. - - ![integrations render authorization](../../images/integrations/render/integrations-render-auth.png) - - - - Select which Infisical environment secrets you want to sync to which Render service and press create integration to start syncing secrets to Render. - - ![integrations render](../../images/integrations/render/integrations-render-create.png) - ![integrations render](../../images/integrations/render/integrations-render.png) - - \ No newline at end of file + + The Render Native Integration will be deprecated in 2026. Please migrate to + our new [Render Sync](../secret-syncs/render). + diff --git a/docs/integrations/secret-syncs/render.mdx b/docs/integrations/secret-syncs/render.mdx new file mode 100644 index 000000000..341d84ad5 --- /dev/null +++ b/docs/integrations/secret-syncs/render.mdx @@ -0,0 +1,135 @@ +--- +title: "Render Sync" +description: "Learn how to configure a Render Sync for Infisical." +--- + +**Prerequisites:** + +- Set up and add secrets to [Infisical Cloud](https://app.infisical.com) +- Create a [Render Connection](/integrations/app-connections/render) + + + + 1. Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button. + ![Secret Syncs Tab](/images/secret-syncs/general/secret-sync-tab.png) + + 2. Select the **Render** option. + ![Select Render](/images/secret-syncs/render/select-render-option.png) + + 3. Configure the **Source** from where secrets should be retrieved, then click **Next**. + ![Configure Source](/images/secret-syncs/render/render-sync-source.png) + + - **Environment**: The project environment to retrieve secrets from. + - **Secret Path**: The folder path to retrieve secrets from. + + + If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports). + + + 4. Configure the **Destination** to where secrets should be deployed, then click **Next**. + ![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. + + 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) + + - **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync. + - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. + - **Import Secrets (Prioritize Infisical)**: Imports secrets from the Render service before syncing, prioritizing values from Infisical over Render when keys conflict. + - **Import Secrets (Prioritize Render)**: Imports secrets from the Render service before syncing, prioritizing values from Render over Infisical when keys conflict. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and `{{environment}}` for the environment. + - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. + - **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical. + + 6. Configure the **Details** of your Render Sync, then click **Next**. + ![Configure Details](/images/secret-syncs/render/render-sync-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + 7. Review your Render Sync configuration, then click **Create Sync**. + ![Confirm Configuration](/images/secret-syncs/render/render-sync-review.png) + + 8. If enabled, your Render Sync will begin syncing your secrets to the destination endpoint. + ![Sync Secrets](/images/secret-syncs/render/render-sync-created.png) + + + + To create a **Render Sync**, make an API request to the [Create Render Sync](/api-reference/endpoints/secret-syncs/render/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/secret-syncs/render \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-render-sync", + "projectId": "your-project-id", + "description": "an example sync", + "connectionId": "your-render-connection-id", + "environment": "production", + "secretPath": "/my-secrets", + "isEnabled": true, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "destinationConfig": { + "scope": "service", + "serviceId": "your-render-service-id", + "type": "env" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "secretSync": { + "id": "your-sync-id", + "name": "my-render-sync", + "description": "an example sync", + "isEnabled": true, + "version": 1, + "folderId": "your-folder-id", + "connectionId": "your-render-connection-id", + "createdAt": "2024-05-01T12:00:00Z", + "updatedAt": "2024-05-01T12:00:00Z", + "syncStatus": "succeeded", + "lastSyncJobId": "123", + "lastSyncMessage": null, + "lastSyncedAt": "2024-05-01T12:00:00Z", + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "projectId": "your-project-id", + "connection": { + "app": "render", + "name": "my-render-connection", + "id": "your-render-connection-id" + }, + "environment": { + "slug": "production", + "name": "Production", + "id": "your-env-id" + }, + "folder": { + "id": "your-folder-id", + "path": "/my-secrets" + }, + "destination": "render", + "destinationConfig": { + "scope": "service", + "serviceId": "your-render-service-id", + "type": "env" + } + } + } + ``` + + + diff --git a/docs/mint.json b/docs/mint.json index c6d657d66..b543ee09f 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -516,6 +516,7 @@ "integrations/app-connections/oci", "integrations/app-connections/oracledb", "integrations/app-connections/postgres", + "integrations/app-connections/render", "integrations/app-connections/teamcity", "integrations/app-connections/terraform-cloud", "integrations/app-connections/vercel", @@ -545,6 +546,7 @@ "integrations/secret-syncs/hashicorp-vault", "integrations/secret-syncs/humanitec", "integrations/secret-syncs/oci-vault", + "integrations/secret-syncs/render", "integrations/secret-syncs/teamcity", "integrations/secret-syncs/terraform-cloud", "integrations/secret-syncs/vercel", @@ -1409,6 +1411,18 @@ "api-reference/endpoints/app-connections/postgres/delete" ] }, + { + "group": "Render", + "pages": [ + "api-reference/endpoints/app-connections/render/list", + "api-reference/endpoints/app-connections/render/available", + "api-reference/endpoints/app-connections/render/get-by-id", + "api-reference/endpoints/app-connections/render/get-by-name", + "api-reference/endpoints/app-connections/render/create", + "api-reference/endpoints/app-connections/render/update", + "api-reference/endpoints/app-connections/render/delete" + ] + }, { "group": "TeamCity", "pages": [ @@ -1655,6 +1669,20 @@ "api-reference/endpoints/secret-syncs/oci-vault/remove-secrets" ] }, + { + "group": "Render", + "pages": [ + "api-reference/endpoints/secret-syncs/render/list", + "api-reference/endpoints/secret-syncs/render/get-by-id", + "api-reference/endpoints/secret-syncs/render/get-by-name", + "api-reference/endpoints/secret-syncs/render/create", + "api-reference/endpoints/secret-syncs/render/update", + "api-reference/endpoints/secret-syncs/render/delete", + "api-reference/endpoints/secret-syncs/render/sync-secrets", + "api-reference/endpoints/secret-syncs/render/import-secrets", + "api-reference/endpoints/secret-syncs/render/remove-secrets" + ] + }, { "group": "TeamCity", "pages": [ diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/RenderSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/RenderSyncFields.tsx new file mode 100644 index 000000000..b5cb407cb --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/RenderSyncFields.tsx @@ -0,0 +1,112 @@ +import { Controller, useFormContext, useWatch } from "react-hook-form"; +import { SingleValue } from "react-select"; + +import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField"; +import { FilterableSelect, FormControl, Select, SelectItem } from "@app/components/v2"; +import { RENDER_SYNC_SCOPES } from "@app/helpers/secretSyncs"; +import { + TRenderService, + useRenderConnectionListServices +} from "@app/hooks/api/appConnections/render"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; +import { RenderSyncScope, RenderSyncType } from "@app/hooks/api/secretSyncs/render-sync"; + +import { TSecretSyncForm } from "../schemas"; + +export const RenderSyncFields = () => { + const { control, setValue } = useFormContext< + TSecretSyncForm & { destination: SecretSync.Render } + >(); + + const connectionId = useWatch({ name: "connection.id", control }); + + const { data: services = [], isPending: isServicesPending } = useRenderConnectionListServices( + connectionId, + { + enabled: Boolean(connectionId) + } + ); + + return ( + <> + { + setValue("destinationConfig.serviceId", ""); + setValue("destinationConfig.type", RenderSyncType.Env); + setValue("destinationConfig.scope", RenderSyncScope.Service); + }} + /> + ( + +

+ Specify how Infisical should manage secrets from Render. The following options are + available: +

+
    + {Object.values(RENDER_SYNC_SCOPES).map(({ name, description }) => { + return ( +
  • +

    + {name}: {description} +

    +
  • + ); + })} +
+ + } + > + +
+ )} + /> + ( + + 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()} + /> + + )} + /> + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx index 37976301a..5d36ac71f 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx @@ -17,6 +17,7 @@ import { GitHubSyncFields } from "./GitHubSyncFields"; import { HCVaultSyncFields } from "./HCVaultSyncFields"; import { HumanitecSyncFields } from "./HumanitecSyncFields"; import { OCIVaultSyncFields } from "./OCIVaultSyncFields"; +import { RenderSyncFields } from "./RenderSyncFields"; import { TeamCitySyncFields } from "./TeamCitySyncFields"; import { TerraformCloudSyncFields } from "./TerraformCloudSyncFields"; import { VercelSyncFields } from "./VercelSyncFields"; @@ -62,6 +63,8 @@ export const SecretSyncDestinationFields = () => { return ; case SecretSync.OnePass: return ; + case SecretSync.Render: + return ; case SecretSync.Flyio: return ; default: diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx index 70c2fc498..9e60e164e 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx @@ -52,6 +52,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => { case SecretSync.TeamCity: case SecretSync.OnePass: case SecretSync.OCIVault: + case SecretSync.Render: case SecretSync.Flyio: AdditionalSyncOptionsFieldsComponent = null; break; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/RenderSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/RenderSyncReviewFields.tsx new file mode 100644 index 000000000..becc46c1d --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/RenderSyncReviewFields.tsx @@ -0,0 +1,18 @@ +import { useFormContext } from "react-hook-form"; + +import { GenericFieldLabel } from "@app/components/secret-syncs"; +import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +export const RenderSyncReviewFields = () => { + const { watch } = useFormContext(); + const serviceName = watch("destinationConfig.serviceName"); + const scope = watch("destinationConfig.scope"); + + return ( + <> + {scope} + {serviceName} + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx index 82d62d19b..a82400f5b 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx @@ -27,6 +27,7 @@ import { HCVaultSyncReviewFields } from "./HCVaultSyncReviewFields"; import { HumanitecSyncReviewFields } from "./HumanitecSyncReviewFields"; import { OCIVaultSyncReviewFields } from "./OCIVaultSyncReviewFields"; import { OnePassSyncReviewFields } from "./OnePassSyncReviewFields"; +import { RenderSyncReviewFields } from "./RenderSyncReviewFields"; import { TeamCitySyncReviewFields } from "./TeamCitySyncReviewFields"; import { TerraformCloudSyncReviewFields } from "./TerraformCloudSyncReviewFields"; import { VercelSyncReviewFields } from "./VercelSyncReviewFields"; @@ -105,6 +106,9 @@ export const SecretSyncReviewFields = () => { case SecretSync.OnePass: DestinationFieldsComponent = ; break; + case SecretSync.Render: + DestinationFieldsComponent = ; + break; case SecretSync.Flyio: DestinationFieldsComponent = ; break; 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 new file mode 100644 index 000000000..16b213421 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/schemas/render-sync-destination-schema.ts @@ -0,0 +1,19 @@ +import { z } from "zod"; + +import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; +import { RenderSyncScope, RenderSyncType } from "@app/hooks/api/secretSyncs/render-sync"; + +export const RenderSyncDestinationSchema = BaseSecretSyncSchema().merge( + z.object({ + destination: z.literal(SecretSync.Render), + destinationConfig: z.discriminatedUnion("scope", [ + z.object({ + scope: z.literal(RenderSyncScope.Service), + serviceId: z.string().trim().min(1, "Service is required"), + serviceName: z.string().trim().optional(), + type: z.nativeEnum(RenderSyncType) + }) + ]) + }) +); diff --git a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts index a10863f17..70cb4767d 100644 --- a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts +++ b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts @@ -14,6 +14,7 @@ import { GitHubSyncDestinationSchema } from "./github-sync-destination-schema"; import { HCVaultSyncDestinationSchema } from "./hc-vault-sync-destination-schema"; import { HumanitecSyncDestinationSchema } from "./humanitec-sync-destination-schema"; import { OCIVaultSyncDestinationSchema } from "./oci-vault-sync-destination-schema"; +import { RenderSyncDestinationSchema } from "./render-sync-destination-schema"; import { TeamCitySyncDestinationSchema } from "./teamcity-sync-destination-schema"; import { TerraformCloudSyncDestinationSchema } from "./terraform-cloud-destination-schema"; import { VercelSyncDestinationSchema } from "./vercel-sync-destination-schema"; @@ -37,6 +38,7 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [ TeamCitySyncDestinationSchema, OCIVaultSyncDestinationSchema, OnePassSyncDestinationSchema, + RenderSyncDestinationSchema, FlyioSyncDestinationSchema ]); diff --git a/frontend/src/helpers/appConnections.ts b/frontend/src/helpers/appConnections.ts index 1efcd23ee..c27e1e646 100644 --- a/frontend/src/helpers/appConnections.ts +++ b/frontend/src/helpers/appConnections.ts @@ -38,6 +38,7 @@ import { WindmillConnectionMethod } from "@app/hooks/api/appConnections/types"; import { OCIConnectionMethod } from "@app/hooks/api/appConnections/types/oci-connection"; +import { RenderConnectionMethod } from "@app/hooks/api/appConnections/types/render-connection"; export const APP_CONNECTION_MAP: Record< AppConnection, @@ -80,6 +81,7 @@ export const APP_CONNECTION_MAP: Record< [AppConnection.TeamCity]: { name: "TeamCity", image: "TeamCity.png" }, [AppConnection.OCI]: { name: "OCI", image: "Oracle.png", enterprise: true }, [AppConnection.OnePass]: { name: "1Password", image: "1Password.png" }, + [AppConnection.Render]: { name: "Render", image: "Render.png" }, [AppConnection.Flyio]: { name: "Fly.io", image: "Flyio.svg" } }; @@ -127,6 +129,8 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) return { name: "App Role", icon: faUser }; case LdapConnectionMethod.SimpleBind: return { name: "Simple Bind", icon: faLink }; + case RenderConnectionMethod.ApiKey: + return { name: "API Key", icon: faKey }; default: throw new Error(`Unhandled App Connection Method: ${method}`); } diff --git a/frontend/src/helpers/secretSyncs.ts b/frontend/src/helpers/secretSyncs.ts index 69d8804f8..c33e45159 100644 --- a/frontend/src/helpers/secretSyncs.ts +++ b/frontend/src/helpers/secretSyncs.ts @@ -4,6 +4,7 @@ import { SecretSyncImportBehavior, SecretSyncInitialSyncBehavior } from "@app/hooks/api/secretSyncs"; +import { RenderSyncScope } from "@app/hooks/api/secretSyncs/render-sync"; import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync"; import { HumanitecSyncScope } from "@app/hooks/api/secretSyncs/types/humanitec-sync"; @@ -61,6 +62,10 @@ export const SECRET_SYNC_MAP: Record = { [SecretSync.TeamCity]: AppConnection.TeamCity, [SecretSync.OCIVault]: AppConnection.OCI, [SecretSync.OnePass]: AppConnection.OnePass, + [SecretSync.Render]: AppConnection.Render, [SecretSync.Flyio]: AppConnection.Flyio }; @@ -146,3 +152,10 @@ export const GCP_SYNC_SCOPES: Record = { + [RenderSyncScope.Service]: { + name: "Service", + description: "Infisical will sync secrets to the specified Render service." + } +}; diff --git a/frontend/src/hooks/api/appConnections/enums.ts b/frontend/src/hooks/api/appConnections/enums.ts index 5073d16b7..1f5cb4a3a 100644 --- a/frontend/src/hooks/api/appConnections/enums.ts +++ b/frontend/src/hooks/api/appConnections/enums.ts @@ -23,5 +23,6 @@ export enum AppConnection { TeamCity = "teamcity", OCI = "oci", OnePass = "1password", + Render = "render", Flyio = "flyio" } diff --git a/frontend/src/hooks/api/appConnections/render/index.ts b/frontend/src/hooks/api/appConnections/render/index.ts new file mode 100644 index 000000000..2c1906d36 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/render/index.ts @@ -0,0 +1,2 @@ +export * from "./queries"; +export * from "./types"; diff --git a/frontend/src/hooks/api/appConnections/render/queries.tsx b/frontend/src/hooks/api/appConnections/render/queries.tsx new file mode 100644 index 000000000..728c46bd9 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/render/queries.tsx @@ -0,0 +1,37 @@ +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { appConnectionKeys } from "../queries"; +import { TRenderService } from "./types"; + +const renderConnectionKeys = { + all: [...appConnectionKeys.all, "render"] as const, + listServices: (connectionId: string) => + [...renderConnectionKeys.all, "services", connectionId] as const +}; + +export const useRenderConnectionListServices = ( + connectionId: string, + options?: Omit< + UseQueryOptions< + TRenderService[], + unknown, + TRenderService[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: renderConnectionKeys.listServices(connectionId), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/app-connections/render/${connectionId}/services` + ); + + return data; + }, + ...options + }); +}; diff --git a/frontend/src/hooks/api/appConnections/render/types.ts b/frontend/src/hooks/api/appConnections/render/types.ts new file mode 100644 index 000000000..ec51adb78 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/render/types.ts @@ -0,0 +1,4 @@ +export type TRenderService = { + id: string; + name: string; +}; diff --git a/frontend/src/hooks/api/appConnections/types/app-options.ts b/frontend/src/hooks/api/appConnections/types/app-options.ts index a1a916896..e6e545daf 100644 --- a/frontend/src/hooks/api/appConnections/types/app-options.ts +++ b/frontend/src/hooks/api/appConnections/types/app-options.ts @@ -110,6 +110,10 @@ export type TOnePassConnectionOption = TAppConnectionOptionBase & { app: AppConnection.OnePass; }; +export type TRenderConnectionOption = TAppConnectionOptionBase & { + app: AppConnection.Render; +}; + export type TFlyioConnectionOption = TAppConnectionOptionBase & { app: AppConnection.Flyio; }; @@ -137,6 +141,7 @@ export type TAppConnectionOption = | TTeamCityConnectionOption | TOCIConnectionOption | TOnePassConnectionOption + | TRenderConnectionOption | TFlyioConnectionOption; export type TAppConnectionOptionMap = { @@ -164,5 +169,6 @@ export type TAppConnectionOptionMap = { [AppConnection.TeamCity]: TTeamCityConnectionOption; [AppConnection.OCI]: TOCIConnectionOption; [AppConnection.OnePass]: TOnePassConnectionOption; + [AppConnection.Render]: TRenderConnectionOption; [AppConnection.Flyio]: TFlyioConnectionOption; }; diff --git a/frontend/src/hooks/api/appConnections/types/index.ts b/frontend/src/hooks/api/appConnections/types/index.ts index 8a57efa35..abaac4fad 100644 --- a/frontend/src/hooks/api/appConnections/types/index.ts +++ b/frontend/src/hooks/api/appConnections/types/index.ts @@ -21,6 +21,7 @@ import { TMySqlConnection } from "./mysql-connection"; import { TOCIConnection } from "./oci-connection"; import { TOracleDBConnection } from "./oracledb-connection"; import { TPostgresConnection } from "./postgres-connection"; +import { TRenderConnection } from "./render-connection"; import { TTeamCityConnection } from "./teamcity-connection"; import { TTerraformCloudConnection } from "./terraform-cloud-connection"; import { TVercelConnection } from "./vercel-connection"; @@ -47,6 +48,7 @@ export * from "./mysql-connection"; export * from "./oci-connection"; export * from "./oracledb-connection"; export * from "./postgres-connection"; +export * from "./render-connection"; export * from "./teamcity-connection"; export * from "./terraform-cloud-connection"; export * from "./vercel-connection"; @@ -77,6 +79,7 @@ export type TAppConnection = | TTeamCityConnection | TOCIConnection | TOnePassConnection + | TRenderConnection | TFlyioConnection; export type TAvailableAppConnection = Pick; @@ -129,5 +132,6 @@ export type TAppConnectionMap = { [AppConnection.TeamCity]: TTeamCityConnection; [AppConnection.OCI]: TOCIConnection; [AppConnection.OnePass]: TOnePassConnection; + [AppConnection.Render]: TRenderConnection; [AppConnection.Flyio]: TFlyioConnection; }; diff --git a/frontend/src/hooks/api/appConnections/types/render-connection.ts b/frontend/src/hooks/api/appConnections/types/render-connection.ts new file mode 100644 index 000000000..bc59c4c39 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/types/render-connection.ts @@ -0,0 +1,13 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection"; + +export enum RenderConnectionMethod { + ApiKey = "api-key" +} + +export type TRenderConnection = TRootAppConnection & { app: AppConnection.Render } & { + method: RenderConnectionMethod.ApiKey; + credentials: { + apiKey: string; + }; +}; diff --git a/frontend/src/hooks/api/secretSyncs/enums.ts b/frontend/src/hooks/api/secretSyncs/enums.ts index 17a5e0983..323266caa 100644 --- a/frontend/src/hooks/api/secretSyncs/enums.ts +++ b/frontend/src/hooks/api/secretSyncs/enums.ts @@ -16,6 +16,7 @@ export enum SecretSync { TeamCity = "teamcity", OCIVault = "oci-vault", OnePass = "1password", + Render = "render", Flyio = "flyio" } diff --git a/frontend/src/hooks/api/secretSyncs/render-sync.ts b/frontend/src/hooks/api/secretSyncs/render-sync.ts new file mode 100644 index 000000000..ecac7d077 --- /dev/null +++ b/frontend/src/hooks/api/secretSyncs/render-sync.ts @@ -0,0 +1,28 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; +import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync"; + +export type TRenderSync = TRootSecretSync & { + destination: SecretSync.Render; + destinationConfig: { + scope: RenderSyncScope.Service; + type: RenderSyncType; + serviceId: string; + serviceName?: string; + }; + + connection: { + app: AppConnection.Render; + name: string; + id: string; + }; +}; + +export enum RenderSyncScope { + Service = "service" +} + +export enum RenderSyncType { + Env = "env", + File = "file" +} diff --git a/frontend/src/hooks/api/secretSyncs/types/index.ts b/frontend/src/hooks/api/secretSyncs/types/index.ts index 0d5bbdb56..fc8a0da46 100644 --- a/frontend/src/hooks/api/secretSyncs/types/index.ts +++ b/frontend/src/hooks/api/secretSyncs/types/index.ts @@ -1,6 +1,7 @@ import { SecretSync, SecretSyncImportBehavior } from "@app/hooks/api/secretSyncs"; import { DiscriminativePick } from "@app/types"; +import { TRenderSync } from "../render-sync"; import { TOnePassSync } from "./1password-sync"; import { TAwsParameterStoreSync } from "./aws-parameter-store-sync"; import { TAwsSecretsManagerSync } from "./aws-secrets-manager-sync"; @@ -45,6 +46,7 @@ export type TSecretSync = | TTeamCitySync | TOCIVaultSync | TOnePassSync + | TRenderSync | TFlyioSync; export type TListSecretSyncs = { secretSyncs: TSecretSync[] }; diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx index 3882df698..99c81a745 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx @@ -30,6 +30,7 @@ import { MySqlConnectionForm } from "./MySqlConnectionForm"; import { OCIConnectionForm } from "./OCIConnectionForm"; import { OracleDBConnectionForm } from "./OracleDBConnectionForm"; import { PostgresConnectionForm } from "./PostgresConnectionForm"; +import { RenderConnectionForm } from "./RenderConnectionForm"; import { TeamCityConnectionForm } from "./TeamCityConnectionForm"; import { TerraformCloudConnectionForm } from "./TerraformCloudConnectionForm"; import { VercelConnectionForm } from "./VercelConnectionForm"; @@ -120,6 +121,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => { return ; case AppConnection.OnePass: return ; + case AppConnection.Render: + return ; case AppConnection.Flyio: return ; default: @@ -206,6 +209,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => { return ; case AppConnection.OnePass: return ; + case AppConnection.Render: + return ; case AppConnection.Flyio: return ; default: diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/RenderConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/RenderConnectionForm.tsx new file mode 100644 index 000000000..adf401afe --- /dev/null +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/RenderConnectionForm.tsx @@ -0,0 +1,132 @@ +import { Controller, FormProvider, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { + Button, + FormControl, + ModalClose, + SecretInput, + Select, + SelectItem +} from "@app/components/v2"; +import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; +import { RenderConnectionMethod, TRenderConnection } from "@app/hooks/api/appConnections"; +import { AppConnection } from "@app/hooks/api/appConnections/enums"; + +import { + genericAppConnectionFieldsSchema, + GenericAppConnectionsFields +} from "./GenericAppConnectionFields"; + +type Props = { + appConnection?: TRenderConnection; + onSubmit: (formData: FormData) => Promise; +}; + +const rootSchema = genericAppConnectionFieldsSchema.extend({ + app: z.literal(AppConnection.Render) +}); + +const formSchema = z.discriminatedUnion("method", [ + rootSchema.extend({ + method: z.literal(RenderConnectionMethod.ApiKey), + credentials: z.object({ + apiKey: z.string().trim().min(1, "API Key required") + }) + }) +]); + +type FormData = z.infer; + +export const RenderConnectionForm = ({ appConnection, onSubmit }: Props) => { + const isUpdate = Boolean(appConnection); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: appConnection ?? { + app: AppConnection.Render, + method: RenderConnectionMethod.ApiKey + } + }); + + const { + handleSubmit, + control, + formState: { isSubmitting, isDirty } + } = form; + + return ( + +
+ {!isUpdate && } + ( + + + + )} + /> + ( + + onChange(e.target.value)} + /> + + )} + /> +
+ + + + +
+ +
+ ); +}; diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/EditAppConnectionDetailsModal.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/EditAppConnectionDetailsModal.tsx index c3e4f13dc..f46a3b3cf 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/EditAppConnectionDetailsModal.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/EditAppConnectionDetailsModal.tsx @@ -7,7 +7,6 @@ import { Button, Modal, ModalClose, ModalContent } from "@app/components/v2"; import { APP_CONNECTION_MAP } from "@app/helpers/appConnections"; import { TAppConnection, useUpdateAppConnection } from "@app/hooks/api/appConnections"; import { AppConnection } from "@app/hooks/api/appConnections/enums"; -import { DiscriminativePick } from "@app/types"; import { genericAppConnectionFieldsSchema, GenericAppConnectionsFields } from "./AppConnectionForm"; @@ -43,7 +42,7 @@ const Content = ({ appConnection, onComplete }: ContentProps) => { formState: { isSubmitting, isDirty } } = form; - const onSubmit = async (formData: DiscriminativePick) => { + const onSubmit = async (formData: FormData) => { try { await updateAppConnection.mutateAsync({ connectionId: appConnection.id, 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 new file mode 100644 index 000000000..43e9e35d2 --- /dev/null +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/RenderSyncDestinationCol.tsx @@ -0,0 +1,29 @@ +import { useRenderConnectionListServices } from "@app/hooks/api/appConnections/render"; +import { TRenderSync } from "@app/hooks/api/secretSyncs/render-sync"; + +import { getSecretSyncDestinationColValues } from "../helpers"; +import { SecretSyncTableCell } from "../SecretSyncTableCell"; + +type Props = { + secretSync: TRenderSync; +}; + +export const RenderSyncDestinationCol = ({ secretSync }: Props) => { + const { data: services = [], isPending } = useRenderConnectionListServices( + secretSync.connectionId + ); + + const { primaryText, secondaryText } = getSecretSyncDestinationColValues({ + ...secretSync, + destinationConfig: { + ...secretSync.destinationConfig, + serviceName: services.find((s) => s.id === secretSync.destinationConfig.serviceId)?.name + } + }); + + if (isPending) { + return ; + } + + return ; +}; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx index 87c03aacb..ca7b37682 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx @@ -14,6 +14,7 @@ import { GitHubSyncDestinationCol } from "./GitHubSyncDestinationCol"; import { HCVaultSyncDestinationCol } from "./HCVaultSyncDestinationCol"; import { HumanitecSyncDestinationCol } from "./HumanitecSyncDestinationCol"; import { OCIVaultSyncDestinationCol } from "./OCIVaultSyncDestinationCol"; +import { RenderSyncDestinationCol } from "./RenderSyncDestinationCol"; import { TeamCitySyncDestinationCol } from "./TeamCitySyncDestinationCol"; import { TerraformCloudSyncDestinationCol } from "./TerraformCloudSyncDestinationCol"; import { VercelSyncDestinationCol } from "./VercelSyncDestinationCol"; @@ -59,6 +60,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => { return ; case SecretSync.AzureDevOps: return ; + case SecretSync.Render: + return ; case SecretSync.Flyio: return ; default: 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 06e9a5a48..f8ab727a2 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 @@ -116,6 +116,10 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => { primaryText = destinationConfig.devopsProjectName; secondaryText = destinationConfig.devopsProjectId; break; + case SecretSync.Render: + primaryText = destinationConfig.serviceName ?? destinationConfig.serviceId; + secondaryText = "Service"; + break; case SecretSync.Flyio: primaryText = destinationConfig.appId; secondaryText = "App ID"; diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/RenderSyncDestinationSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/RenderSyncDestinationSection.tsx new file mode 100644 index 000000000..b661caf00 --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/RenderSyncDestinationSection.tsx @@ -0,0 +1,23 @@ +import { GenericFieldLabel } from "@app/components/secret-syncs"; +import { useRenderConnectionListServices } from "@app/hooks/api/appConnections/render"; +import { TRenderSync } from "@app/hooks/api/secretSyncs/render-sync"; + +type Props = { + secretSync: TRenderSync; +}; + +export const RenderSyncDestinationSection = ({ secretSync }: Props) => { + const { data: services = [], isPending } = useRenderConnectionListServices( + secretSync.connectionId + ); + const { + destinationConfig: { serviceId } + } = secretSync; + + if (isPending) { + return Loading...; + } + + const serviceName = services.find((service) => service.id === serviceId)?.name; + return {serviceName ?? serviceId}; +}; diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx index 0ed9a8f36..bd245cc19 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx @@ -25,6 +25,7 @@ import { GitHubSyncDestinationSection } from "./GitHubSyncDestinationSection"; import { HCVaultSyncDestinationSection } from "./HCVaultSyncDestinationSection"; import { HumanitecSyncDestinationSection } from "./HumanitecSyncDestinationSection"; import { OCIVaultSyncDestinationSection } from "./OCIVaultSyncDestinationSection"; +import { RenderSyncDestinationSection } from "./RenderSyncDestinationSection"; import { TeamCitySyncDestinationSection } from "./TeamCitySyncDestinationSection"; import { TerraformCloudSyncDestinationSection } from "./TerraformCloudSyncDestinationSection"; import { VercelSyncDestinationSection } from "./VercelSyncDestinationSection"; @@ -95,6 +96,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }: case SecretSync.AzureDevOps: DestinationComponents = ; break; + case SecretSync.Render: + DestinationComponents = ; + break; case SecretSync.Flyio: DestinationComponents = ; break; diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx index b90f57d2c..213a08bdc 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx @@ -55,6 +55,7 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) = case SecretSync.TeamCity: case SecretSync.OCIVault: case SecretSync.OnePass: + case SecretSync.Render: case SecretSync.Flyio: AdditionalSyncOptionsComponent = null; break;