diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index b6c00985a..318c085a1 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -2373,6 +2373,10 @@ export const SecretSyncs = { keyId: "The AWS KMS key ID or alias to use when encrypting parameters synced by Infisical.", tags: "Optional tags to add to secrets synced by Infisical.", syncSecretMetadataAsTags: `Whether Infisical secret metadata should be added as tags to secrets synced by Infisical.` + }, + RENDER: { + autoRedeployServices: + "Whether Infisical should automatically redeploy the configured Render service upon secret changes." } }, DESTINATION_CONFIG: { 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 36e97e620..8af3653ca 100644 --- a/backend/src/services/secret-sync/render/render-sync-fns.ts +++ b/backend/src/services/secret-sync/render/render-sync-fns.ts @@ -97,6 +97,28 @@ const batchUpdateEnvironmentSecrets = async ( ); }; +const redeployService = async (secretSync: TRenderSyncWithCredentials) => { + const { + destinationConfig, + connection: { + credentials: { apiKey } + } + } = secretSync; + + await makeRequestWithRetry(() => + request.post( + `${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/deploys`, + {}, + { + headers: { + Authorization: `Bearer ${apiKey}`, + "Accept-Encoding": "application/json" + } + } + ) + ); +}; + export const RenderSyncFns = { syncSecrets: async (secretSync: TRenderSyncWithCredentials, secretMap: TSecretMap) => { const renderSecrets = await getRenderEnvironmentSecrets(secretSync); @@ -131,6 +153,10 @@ export const RenderSyncFns = { } await batchUpdateEnvironmentSecrets(secretSync, finalEnvVars); + + if (secretSync.syncOptions.autoRedeployServices) { + await redeployService(secretSync); + } }, getSecrets: async (secretSync: TRenderSyncWithCredentials): Promise => { @@ -151,5 +177,9 @@ export const RenderSyncFns = { } } await batchUpdateEnvironmentSecrets(secretSync, finalEnvVars); + + 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 77414c17c..0d6e93987 100644 --- a/backend/src/services/secret-sync/render/render-sync-schemas.ts +++ b/backend/src/services/secret-sync/render/render-sync-schemas.ts @@ -20,23 +20,33 @@ const RenderSyncDestinationConfigSchema = z.discriminatedUnion("scope", [ }) ]); +const RenderSyncOptionsSchema = z.object({ + autoRedeployServices: z.boolean().optional().describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.RENDER.autoRedeployServices) +}); + const RenderSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true }; -export const RenderSyncSchema = BaseSecretSyncSchema(SecretSync.Render, RenderSyncOptionsConfig).extend({ +export const RenderSyncSchema = BaseSecretSyncSchema( + SecretSync.Render, + RenderSyncOptionsConfig, + RenderSyncOptionsSchema +).extend({ destination: z.literal(SecretSync.Render), destinationConfig: RenderSyncDestinationConfigSchema }); export const CreateRenderSyncSchema = GenericCreateSecretSyncFieldsSchema( SecretSync.Render, - RenderSyncOptionsConfig + RenderSyncOptionsConfig, + RenderSyncOptionsSchema ).extend({ destinationConfig: RenderSyncDestinationConfigSchema }); export const UpdateRenderSyncSchema = GenericUpdateSecretSyncFieldsSchema( SecretSync.Render, - RenderSyncOptionsConfig + RenderSyncOptionsConfig, + RenderSyncOptionsSchema ).extend({ destinationConfig: RenderSyncDestinationConfigSchema.optional() }); diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/RenderSyncOptionsFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/RenderSyncOptionsFields.tsx new file mode 100644 index 000000000..6d4173bd0 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/RenderSyncOptionsFields.tsx @@ -0,0 +1,40 @@ +import { Controller, useFormContext } from "react-hook-form"; +import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { FormControl, Switch, Tooltip } from "@app/components/v2"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +import { TSecretSyncForm } from "../schemas"; + +export const RenderSyncOptionsFields = () => { + const { control } = useFormContext(); + + return ( + ( + + + Auto Redeploy Services On Sync + If enabled, services will be automatically redeployed upon secret changes.

+ } + > + +
+
+
+ )} + /> + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx index 50ea46ac0..cb7a40559 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx @@ -14,6 +14,7 @@ import { SecretSync, useSecretSyncOption } from "@app/hooks/api/secretSyncs"; import { TSecretSyncForm } from "../schemas"; import { AwsParameterStoreSyncOptionsFields } from "./AwsParameterStoreSyncOptionsFields"; import { AwsSecretsManagerSyncOptionsFields } from "./AwsSecretsManagerSyncOptionsFields"; +import { RenderSyncOptionsFields } from "./RenderSyncOptionsFields"; type Props = { hideInitialSync?: boolean; @@ -38,6 +39,9 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => { case SecretSync.AWSSecretsManager: AdditionalSyncOptionsFieldsComponent = ; break; + case SecretSync.Render: + AdditionalSyncOptionsFieldsComponent = ; + break; case SecretSync.GitHub: case SecretSync.GCPSecretManager: case SecretSync.AzureKeyVault: @@ -54,7 +58,6 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => { case SecretSync.OnePass: case SecretSync.OCIVault: case SecretSync.Heroku: - case SecretSync.Render: case SecretSync.Flyio: case SecretSync.GitLab: case SecretSync.CloudflarePages: diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/RenderSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/RenderSyncReviewFields.tsx index becc46c1d..15f5b6625 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/RenderSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/RenderSyncReviewFields.tsx @@ -2,8 +2,29 @@ import { useFormContext } from "react-hook-form"; 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"; +export const RenderSyncOptionsReviewFields = () => { + const { watch } = useFormContext(); + + const [{ autoRedeployServices }] = watch(["syncOptions"]); + + return ( +
+ {autoRedeployServices ? ( + + Enabled + + ) : ( + + Disabled + + )} +
+ ); +}; + export const RenderSyncReviewFields = () => { const { watch } = useFormContext(); const serviceName = watch("destinationConfig.serviceName"); diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx index c1194a4c1..5ee53f2e3 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx @@ -35,7 +35,7 @@ import { HumanitecSyncReviewFields } from "./HumanitecSyncReviewFields"; import { OCIVaultSyncReviewFields } from "./OCIVaultSyncReviewFields"; import { OnePassSyncReviewFields } from "./OnePassSyncReviewFields"; import { RailwaySyncReviewFields } from "./RailwaySyncReviewFields"; -import { RenderSyncReviewFields } from "./RenderSyncReviewFields"; +import { RenderSyncOptionsReviewFields, RenderSyncReviewFields } from "./RenderSyncReviewFields"; import { SupabaseSyncReviewFields } from "./SupabaseSyncReviewFields"; import { TeamCitySyncReviewFields } from "./TeamCitySyncReviewFields"; import { TerraformCloudSyncReviewFields } from "./TerraformCloudSyncReviewFields"; @@ -121,6 +121,7 @@ export const SecretSyncReviewFields = () => { break; case SecretSync.Render: DestinationFieldsComponent = ; + AdditionalSyncOptionsFieldsComponent = ; break; case SecretSync.Flyio: DestinationFieldsComponent = ; 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 16b213421..da81e121d 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 @@ -4,7 +4,11 @@ import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas import { SecretSync } from "@app/hooks/api/secretSyncs"; import { RenderSyncScope, RenderSyncType } from "@app/hooks/api/secretSyncs/render-sync"; -export const RenderSyncDestinationSchema = BaseSecretSyncSchema().merge( +export const RenderSyncDestinationSchema = BaseSecretSyncSchema( + z.object({ + autoRedeployServices: z.boolean().optional() + }) +).merge( z.object({ destination: z.literal(SecretSync.Render), destinationConfig: z.discriminatedUnion("scope", [ diff --git a/frontend/src/hooks/api/secretSyncs/render-sync.ts b/frontend/src/hooks/api/secretSyncs/render-sync.ts index ecac7d077..61aecc53f 100644 --- a/frontend/src/hooks/api/secretSyncs/render-sync.ts +++ b/frontend/src/hooks/api/secretSyncs/render-sync.ts @@ -16,6 +16,10 @@ export type TRenderSync = TRootSecretSync & { name: string; id: string; }; + + syncOptions: { + autoRedeployServices?: boolean; + }; }; export enum RenderSyncScope { diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/RenderSyncOptionsSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/RenderSyncOptionsSection.tsx new file mode 100644 index 000000000..3dd3d2fd5 --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/RenderSyncOptionsSection.tsx @@ -0,0 +1,27 @@ +import { GenericFieldLabel } from "@app/components/secret-syncs"; +import { Badge } from "@app/components/v2"; +import { TRenderSync } from "@app/hooks/api/secretSyncs/render-sync"; + +type Props = { + secretSync: TRenderSync; +}; + +export const RenderSyncOptionsSection = ({ secretSync }: Props) => { + const { + syncOptions: { autoRedeployServices } + } = secretSync; + + return ( +
+ {autoRedeployServices ? ( + + Enabled + + ) : ( + + Disabled + + )} +
+ ); +}; 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 843e02f63..32c46fca2 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx @@ -13,6 +13,7 @@ import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs"; import { AwsParameterStoreSyncOptionsSection } from "./AwsParameterStoreSyncOptionsSection"; import { AwsSecretsManagerSyncOptionsSection } from "./AwsSecretsManagerSyncOptionsSection"; +import { RenderSyncOptionsSection } from "./RenderSyncOptionsSection"; type Props = { secretSync: TSecretSync; @@ -40,6 +41,9 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) = ); break; + case SecretSync.Render: + AdditionalSyncOptionsComponent = ; + break; case SecretSync.GitHub: case SecretSync.GCPSecretManager: case SecretSync.AzureKeyVault: @@ -56,7 +60,6 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) = case SecretSync.OCIVault: case SecretSync.OnePass: case SecretSync.Heroku: - case SecretSync.Render: case SecretSync.Flyio: case SecretSync.GitLab: case SecretSync.CloudflarePages: