diff --git a/backend/src/db/migrations/20250718133527_project-unify-revert.ts b/backend/src/db/migrations/20250718133527_project-unify-revert.ts index 53f1be9e4..d18c38e34 100644 --- a/backend/src/db/migrations/20250718133527_project-unify-revert.ts +++ b/backend/src/db/migrations/20250718133527_project-unify-revert.ts @@ -430,4 +430,3 @@ export async function down(knex: Knex): Promise { }); } } - diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index ac4ee26fb..b6c00985a 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -2290,6 +2290,9 @@ export const AppConnections = { accessKey: "The Key used to access Supabase.", instanceUrl: "The URL used to access Supabase." }, + DIGITAL_OCEAN_APP_PLATFORM: { + apiToken: "The API token used to authenticate with Digital Ocean App Platform." + }, OKTA: { instanceUrl: "The URL used to access your Okta organization.", apiToken: "The API token used to authenticate with Okta." 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 ba0826f87..8468d7305 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 @@ -51,6 +51,10 @@ import { DatabricksConnectionListItemSchema, SanitizedDatabricksConnectionSchema } from "@app/services/app-connection/databricks"; +import { + DigitalOceanConnectionListItemSchema, + SanitizedDigitalOceanConnectionSchema +} from "@app/services/app-connection/digital-ocean"; import { FlyioConnectionListItemSchema, SanitizedFlyioConnectionSchema } from "@app/services/app-connection/flyio"; import { GcpConnectionListItemSchema, SanitizedGcpConnectionSchema } from "@app/services/app-connection/gcp"; import { GitHubConnectionListItemSchema, SanitizedGitHubConnectionSchema } from "@app/services/app-connection/github"; @@ -140,6 +144,7 @@ const SanitizedAppConnectionSchema = z.union([ ...SanitizedRailwayConnectionSchema.options, ...SanitizedChecklyConnectionSchema.options, ...SanitizedSupabaseConnectionSchema.options, + ...SanitizedDigitalOceanConnectionSchema.options, ...SanitizedOktaConnectionSchema.options ]); @@ -178,6 +183,7 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ RailwayConnectionListItemSchema, ChecklyConnectionListItemSchema, SupabaseConnectionListItemSchema, + DigitalOceanConnectionListItemSchema, OktaConnectionListItemSchema ]); diff --git a/backend/src/server/routes/v1/app-connection-routers/digital-ocean-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/digital-ocean-connection-router.ts new file mode 100644 index 000000000..cc8498213 --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/digital-ocean-connection-router.ts @@ -0,0 +1,57 @@ +import { z } from "zod"; + +import { readLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + CreateDigitalOceanConnectionSchema, + SanitizedDigitalOceanConnectionSchema, + UpdateDigitalOceanConnectionSchema +} from "@app/services/app-connection/digital-ocean"; +import { AuthMode } from "@app/services/auth/auth-type"; + +import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; + +export const registerDigitalOceanConnectionRouter = async (server: FastifyZodProvider) => { + registerAppConnectionEndpoints({ + app: AppConnection.DigitalOcean, + server, + createSchema: CreateDigitalOceanConnectionSchema, + updateSchema: UpdateDigitalOceanConnectionSchema, + sanitizedResponseSchema: SanitizedDigitalOceanConnectionSchema + }); + + // The below endpoints are not exposed and for Infisical App use + server.route({ + method: "GET", + url: `/:connectionId/apps`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + response: { + 200: z.object({ + apps: z + .object({ + id: z.string(), + spec: z.object({ + name: z.string() + }) + }) + .array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { connectionId } = req.params; + + const apps = await server.services.appConnection.digitalOcean.listApps(connectionId, req.permission); + + return { apps }; + } + }); +}; 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 3bbdc363d..6744e46b3 100644 --- a/backend/src/server/routes/v1/app-connection-routers/index.ts +++ b/backend/src/server/routes/v1/app-connection-routers/index.ts @@ -14,6 +14,7 @@ import { registerCamundaConnectionRouter } from "./camunda-connection-router"; import { registerChecklyConnectionRouter } from "./checkly-connection-router"; import { registerCloudflareConnectionRouter } from "./cloudflare-connection-router"; import { registerDatabricksConnectionRouter } from "./databricks-connection-router"; +import { registerDigitalOceanConnectionRouter } from "./digital-ocean-connection-router"; import { registerFlyioConnectionRouter } from "./flyio-connection-router"; import { registerGcpConnectionRouter } from "./gcp-connection-router"; import { registerGitHubConnectionRouter } from "./github-connection-router"; @@ -74,5 +75,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record + registerSyncSecretsEndpoints({ + destination: SecretSync.DigitalOceanAppPlatform, + server, + responseSchema: DigitalOceanAppPlatformSyncSchema, + createSchema: CreateDigitalOceanAppPlatformSyncSchema, + updateSchema: UpdateDigitalOceanAppPlatformSyncSchema + }); 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 1b640acae..03cf9d053 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/index.ts +++ b/backend/src/server/routes/v1/secret-sync-routers/index.ts @@ -13,6 +13,7 @@ import { registerChecklySyncRouter } from "./checkly-sync-router"; import { registerCloudflarePagesSyncRouter } from "./cloudflare-pages-sync-router"; import { registerCloudflareWorkersSyncRouter } from "./cloudflare-workers-sync-router"; import { registerDatabricksSyncRouter } from "./databricks-sync-router"; +import { registerDigitalOceanAppPlatformSyncRouter } from "./digital-ocean-app-platform-sync-router"; import { registerFlyioSyncRouter } from "./flyio-sync-router"; import { registerGcpSyncRouter } from "./gcp-sync-router"; import { registerGitHubSyncRouter } from "./github-sync-router"; @@ -59,5 +60,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record { getBitbucketConnectionListItem(), getChecklyConnectionListItem(), getSupabaseConnectionListItem(), + getDigitalOceanConnectionListItem(), getOktaConnectionListItem() ].sort((a, b) => a.name.localeCompare(b.name)); }; @@ -244,6 +250,7 @@ export const validateAppConnectionCredentials = async ( [AppConnection.Bitbucket]: validateBitbucketConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Checkly]: validateChecklyConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Supabase]: validateSupabaseConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.DigitalOcean]: validateDigitalOceanConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Okta]: validateOktaConnectionCredentials as TAppConnectionCredentialsValidator }; @@ -283,6 +290,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) => case CloudflareConnectionMethod.APIToken: case BitbucketConnectionMethod.ApiToken: case ZabbixConnectionMethod.ApiToken: + case DigitalOceanConnectionMethod.ApiToken: case OktaConnectionMethod.ApiToken: return "API Token"; case PostgresConnectionMethod.UsernameAndPassword: @@ -372,6 +380,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record< [AppConnection.Bitbucket]: platformManagedCredentialsNotSupported, [AppConnection.Checkly]: platformManagedCredentialsNotSupported, [AppConnection.Supabase]: platformManagedCredentialsNotSupported, + [AppConnection.DigitalOcean]: platformManagedCredentialsNotSupported, [AppConnection.Okta]: platformManagedCredentialsNotSupported }; diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts index 03b979312..603d30ccd 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -35,6 +35,7 @@ export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.Bitbucket]: "Bitbucket", [AppConnection.Checkly]: "Checkly", [AppConnection.Supabase]: "Supabase", + [AppConnection.DigitalOcean]: "DigitalOcean App Platform", [AppConnection.Okta]: "Okta" }; @@ -73,5 +74,6 @@ export const APP_CONNECTION_PLAN_MAP: Record { + return { + name: "Digital Ocean" as z.infer["name"], + app: AppConnection.DigitalOcean as const, + methods: Object.values(DigitalOceanConnectionMethod) + }; +}; + +export const validateDigitalOceanConnectionCredentials = async (config: TDigitalOceanConnectionConfig) => { + try { + await DigitalOceanAppPlatformPublicAPI.healthcheck(config); + } 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 config.credentials; +}; diff --git a/backend/src/services/app-connection/digital-ocean/digital-ocean-connection-public-client.ts b/backend/src/services/app-connection/digital-ocean/digital-ocean-connection-public-client.ts new file mode 100644 index 000000000..dcf3ed911 --- /dev/null +++ b/backend/src/services/app-connection/digital-ocean/digital-ocean-connection-public-client.ts @@ -0,0 +1,105 @@ +/* eslint-disable no-await-in-loop */ +/* eslint-disable class-methods-use-this */ +import { AxiosInstance } from "axios"; + +import { createRequestClient } from "@app/lib/config/request"; +import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; + +import { DigitalOceanConnectionMethod } from "./digital-ocean-connection-constants"; +import { + TDigitalOceanApp, + TDigitalOceanConnectionConfig, + TDigitalOceanVariable +} from "./digital-ocean-connection-types"; + +class DigitalOceanAppPlatformPublicClient { + private readonly client: AxiosInstance; + + constructor() { + this.client = createRequestClient({ + baseURL: `${IntegrationUrls.DIGITAL_OCEAN_API_URL}/v2`, + headers: { + "Content-Type": "application/json", + Accept: "application/json" + } + }); + } + + async healthcheck(connection: TDigitalOceanConnectionConfig) { + switch (connection.method) { + case DigitalOceanConnectionMethod.ApiToken: + await this.getApps(connection); + break; + default: + throw new Error(`Unsupported connection method`); + } + } + + async getApps(connection: TDigitalOceanConnectionConfig) { + const response = await this.client.get<{ apps: TDigitalOceanApp[] }>(`/apps`, { + headers: { + Authorization: `Bearer ${connection.credentials.apiToken}` + } + }); + + return response.data.apps; + } + + async getApp(connection: TDigitalOceanConnectionConfig, appId: string) { + const response = await this.client.get<{ app: TDigitalOceanApp }>(`/apps/${appId}`, { + headers: { + Authorization: `Bearer ${connection.credentials.apiToken}` + } + }); + + return response.data.app; + } + + async getVariables(connection: TDigitalOceanConnectionConfig, appId: string): Promise { + const app = await this.getApp(connection, appId); + return app.spec.envs || []; + } + + async putVariables(connection: TDigitalOceanConnectionConfig, appId: string, ...input: TDigitalOceanVariable[]) { + const response = await this.getApp(connection, appId); + + return this.client.put( + `/apps/${appId}`, + { + spec: { + ...response.spec, + envs: input + } + }, + { + headers: { + Authorization: `Bearer ${connection.credentials.apiToken}` + } + } + ); + } + + async deleteVariables(connection: TDigitalOceanConnectionConfig, appId: string, ...input: TDigitalOceanVariable[]) { + const response = await this.getApp(connection, appId); + const existing = response.spec.envs || []; + + const variables = existing.filter((v) => input.find((i) => i.key === v.key)); + + return this.client.put( + `/apps/${appId}`, + { + spec: { + ...response.spec, + envs: variables + } + }, + { + headers: { + Authorization: `Bearer ${connection.credentials.apiToken}` + } + } + ); + } +} + +export const DigitalOceanAppPlatformPublicAPI = new DigitalOceanAppPlatformPublicClient(); diff --git a/backend/src/services/app-connection/digital-ocean/digital-ocean-connection-schemas.ts b/backend/src/services/app-connection/digital-ocean/digital-ocean-connection-schemas.ts new file mode 100644 index 000000000..449721a3d --- /dev/null +++ b/backend/src/services/app-connection/digital-ocean/digital-ocean-connection-schemas.ts @@ -0,0 +1,67 @@ +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 { DigitalOceanConnectionMethod } from "./digital-ocean-connection-constants"; + +export const DigitalOceanConnectionMethodSchema = z + .nativeEnum(DigitalOceanConnectionMethod) + .describe(AppConnections.CREATE(AppConnection.DigitalOcean).method); + +export const DigitalOceanConnectionAccessTokenCredentialsSchema = z.object({ + apiToken: z + .string() + .trim() + .min(1, "API Token required") + .max(255) + .describe(AppConnections.CREDENTIALS.DIGITAL_OCEAN_APP_PLATFORM.apiToken) +}); + +const BaseDigitalOceanConnectionSchema = BaseAppConnectionSchema.extend({ + app: z.literal(AppConnection.DigitalOcean) +}); + +export const DigitalOceanConnectionSchema = BaseDigitalOceanConnectionSchema.extend({ + method: DigitalOceanConnectionMethodSchema, + credentials: DigitalOceanConnectionAccessTokenCredentialsSchema +}); + +export const SanitizedDigitalOceanConnectionSchema = z.discriminatedUnion("method", [ + BaseDigitalOceanConnectionSchema.extend({ + method: DigitalOceanConnectionMethodSchema, + credentials: DigitalOceanConnectionAccessTokenCredentialsSchema.pick({}) + }) +]); + +export const ValidateDigitalOceanConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: DigitalOceanConnectionMethodSchema, + credentials: DigitalOceanConnectionAccessTokenCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.DigitalOcean).credentials + ) + }) +]); + +export const CreateDigitalOceanConnectionSchema = ValidateDigitalOceanConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.DigitalOcean) +); + +export const UpdateDigitalOceanConnectionSchema = z + .object({ + credentials: DigitalOceanConnectionAccessTokenCredentialsSchema.optional().describe( + AppConnections.UPDATE(AppConnection.DigitalOcean).credentials + ) + }) + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.DigitalOcean)); + +export const DigitalOceanConnectionListItemSchema = z.object({ + name: z.literal("Digital Ocean"), + app: z.literal(AppConnection.DigitalOcean), + methods: z.nativeEnum(DigitalOceanConnectionMethod).array() +}); diff --git a/backend/src/services/app-connection/digital-ocean/digital-ocean-connection-service.ts b/backend/src/services/app-connection/digital-ocean/digital-ocean-connection-service.ts new file mode 100644 index 000000000..97d4f700f --- /dev/null +++ b/backend/src/services/app-connection/digital-ocean/digital-ocean-connection-service.ts @@ -0,0 +1,29 @@ +import { logger } from "@app/lib/logger"; +import { OrgServiceActor } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { DigitalOceanAppPlatformPublicAPI } from "./digital-ocean-connection-public-client"; +import { TDigitalOceanConnection } from "./digital-ocean-connection-types"; + +type TGetAppConnectionFunc = ( + app: AppConnection, + connectionId: string, + actor: OrgServiceActor +) => Promise; + +export const digitalOceanAppPlatformConnectionService = (getAppConnection: TGetAppConnectionFunc) => { + const listApps = async (connectionId: string, actor: OrgServiceActor) => { + const connection = await getAppConnection(AppConnection.DigitalOcean, connectionId, actor); + try { + const apps = await DigitalOceanAppPlatformPublicAPI.getApps(connection); + return apps; + } catch (error) { + logger.error(error, "Failed to list apps on Digital Ocean"); + return []; + } + }; + + return { + listApps + }; +}; diff --git a/backend/src/services/app-connection/digital-ocean/digital-ocean-connection-types.ts b/backend/src/services/app-connection/digital-ocean/digital-ocean-connection-types.ts new file mode 100644 index 000000000..cc6b4c1ef --- /dev/null +++ b/backend/src/services/app-connection/digital-ocean/digital-ocean-connection-types.ts @@ -0,0 +1,42 @@ +import z from "zod"; + +import { DiscriminativePick } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { + CreateDigitalOceanConnectionSchema, + DigitalOceanConnectionSchema, + ValidateDigitalOceanConnectionCredentialsSchema +} from "./digital-ocean-connection-schemas"; + +export type TDigitalOceanConnection = z.infer; + +export type TDigitalOceanConnectionInput = z.infer & { + app: AppConnection.DigitalOcean; +}; + +export type TValidateDigitalOceanCredentialsSchema = typeof ValidateDigitalOceanConnectionCredentialsSchema; + +export type TDigitalOceanConnectionConfig = DiscriminativePick< + TDigitalOceanConnection, + "method" | "app" | "credentials" +> & { + orgId: string; +}; + +export type TDigitalOceanVariable = { + key: string; + value: string; + type: "SECRET" | "GENERAL"; +}; + +export type TDigitalOceanApp = { + id: string; + spec: { + name: string; + services: Array<{ + name: string; + }>; + envs?: TDigitalOceanVariable[]; + }; +}; diff --git a/backend/src/services/app-connection/digital-ocean/index.ts b/backend/src/services/app-connection/digital-ocean/index.ts new file mode 100644 index 000000000..8c8d483b7 --- /dev/null +++ b/backend/src/services/app-connection/digital-ocean/index.ts @@ -0,0 +1,4 @@ +export * from "./digital-ocean-connection-constants"; +export * from "./digital-ocean-connection-fns"; +export * from "./digital-ocean-connection-schemas"; +export * from "./digital-ocean-connection-types"; diff --git a/backend/src/services/secret-sync/digital-ocean-app-platform/digital-ocean-app-platform-sync-constants.ts b/backend/src/services/secret-sync/digital-ocean-app-platform/digital-ocean-app-platform-sync-constants.ts new file mode 100644 index 000000000..22dc48b3f --- /dev/null +++ b/backend/src/services/secret-sync/digital-ocean-app-platform/digital-ocean-app-platform-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 DIGITAL_OCEAN_APP_PLATFORM_SYNC_LIST_OPTION: TSecretSyncListItem = { + name: "Digital Ocean App Platform" as const, + destination: SecretSync.DigitalOceanAppPlatform, + connection: AppConnection.DigitalOcean, + canImportSecrets: false +}; diff --git a/backend/src/services/secret-sync/digital-ocean-app-platform/digital-ocean-app-platform-sync-fns.ts b/backend/src/services/secret-sync/digital-ocean-app-platform/digital-ocean-app-platform-sync-fns.ts new file mode 100644 index 000000000..a2474fd2c --- /dev/null +++ b/backend/src/services/secret-sync/digital-ocean-app-platform/digital-ocean-app-platform-sync-fns.ts @@ -0,0 +1,83 @@ +/* eslint-disable no-continue */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ + +import { TDigitalOceanVariable } from "@app/services/app-connection/digital-ocean"; +import { DigitalOceanAppPlatformPublicAPI } from "@app/services/app-connection/digital-ocean/digital-ocean-connection-public-client"; +import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns"; + +import { SecretSyncError } from "../secret-sync-errors"; +import { SECRET_SYNC_NAME_MAP } from "../secret-sync-maps"; +import { TSecretMap } from "../secret-sync-types"; +import { TDigitalOceanAppPlatformSyncWithCredentials } from "./digital-ocean-app-platform-sync-types"; + +export const DigitalOceanAppPlatformSyncFns = { + async getSecrets(secretSync: TDigitalOceanAppPlatformSyncWithCredentials) { + throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`); + }, + + async syncSecrets(secretSync: TDigitalOceanAppPlatformSyncWithCredentials, secretMap: TSecretMap) { + const { + environment, + syncOptions: { disableSecretDeletion, keySchema } + } = secretSync; + + const config = secretSync.destinationConfig; + + const existing = await DigitalOceanAppPlatformPublicAPI.getVariables(secretSync.connection, config.appId); + + const variables: Record = Object.fromEntries(existing.map((v) => [v.key, v])); + + for (const [key, value] of Object.entries(secretMap)) { + variables[key] = { + key, + value: value.value, + type: "SECRET" + } as TDigitalOceanVariable; + } + + if (!disableSecretDeletion) { + for (const v of existing) { + if (!matchesSchema(v.key, environment?.slug || "", keySchema)) continue; + if (!(v.key in secretMap)) { + delete variables[v.key]; + } + } + } + + try { + const vars = Object.values(variables); + await DigitalOceanAppPlatformPublicAPI.putVariables(secretSync.connection, config.appId, ...vars); + } catch (error) { + throw new SecretSyncError({ + error + }); + } + }, + + async removeSecrets(secretSync: TDigitalOceanAppPlatformSyncWithCredentials, secretMap: TSecretMap) { + const config = secretSync.destinationConfig; + + try { + const existingSecrets = await DigitalOceanAppPlatformPublicAPI.getVariables(secretSync.connection, config.appId); + + const vars = Object.entries(existingSecrets) + .map(([key, v]) => { + if (!(key in secretMap)) return; + + return { + key, + value: v.value, + type: "SECRET" + } as TDigitalOceanVariable; + }) + .filter(Boolean) as TDigitalOceanVariable[]; + + await DigitalOceanAppPlatformPublicAPI.deleteVariables(secretSync.connection, config.appId, ...vars); + } catch (error) { + throw new SecretSyncError({ + error + }); + } + } +}; diff --git a/backend/src/services/secret-sync/digital-ocean-app-platform/digital-ocean-app-platform-sync-schemas.ts b/backend/src/services/secret-sync/digital-ocean-app-platform/digital-ocean-app-platform-sync-schemas.ts new file mode 100644 index 000000000..09b943d16 --- /dev/null +++ b/backend/src/services/secret-sync/digital-ocean-app-platform/digital-ocean-app-platform-sync-schemas.ts @@ -0,0 +1,46 @@ +import { z } from "zod"; + +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"; + +const DigitalOceanAppPlatformSyncDestinationConfigSchema = z.object({ + appId: z.string().min(1, "Account ID is required").max(255, "Account ID must be less than 255 characters"), + appName: z.string().min(1, "Account Name is required").max(255, "Account Name must be less than 255 characters") +}); + +const DigitalOceanAppPlatformSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false }; + +export const DigitalOceanAppPlatformSyncSchema = BaseSecretSyncSchema( + SecretSync.DigitalOceanAppPlatform, + DigitalOceanAppPlatformSyncOptionsConfig +).extend({ + destination: z.literal(SecretSync.DigitalOceanAppPlatform), + destinationConfig: DigitalOceanAppPlatformSyncDestinationConfigSchema +}); + +export const CreateDigitalOceanAppPlatformSyncSchema = GenericCreateSecretSyncFieldsSchema( + SecretSync.DigitalOceanAppPlatform, + DigitalOceanAppPlatformSyncOptionsConfig +).extend({ + destinationConfig: DigitalOceanAppPlatformSyncDestinationConfigSchema +}); + +export const UpdateDigitalOceanAppPlatformSyncSchema = GenericUpdateSecretSyncFieldsSchema( + SecretSync.DigitalOceanAppPlatform, + DigitalOceanAppPlatformSyncOptionsConfig +).extend({ + destinationConfig: DigitalOceanAppPlatformSyncDestinationConfigSchema.optional() +}); + +export const DigitalOceanAppPlatformSyncListItemSchema = z.object({ + name: z.literal("Digital Ocean App Platform"), + connection: z.literal(AppConnection.DigitalOcean), + destination: z.literal(SecretSync.DigitalOceanAppPlatform), + canImportSecrets: z.literal(false) +}); diff --git a/backend/src/services/secret-sync/digital-ocean-app-platform/digital-ocean-app-platform-sync-types.ts b/backend/src/services/secret-sync/digital-ocean-app-platform/digital-ocean-app-platform-sync-types.ts new file mode 100644 index 000000000..87832079b --- /dev/null +++ b/backend/src/services/secret-sync/digital-ocean-app-platform/digital-ocean-app-platform-sync-types.ts @@ -0,0 +1,23 @@ +import z from "zod"; + +import { TDigitalOceanConnection, TDigitalOceanVariable } from "@app/services/app-connection/digital-ocean"; + +import { + CreateDigitalOceanAppPlatformSyncSchema, + DigitalOceanAppPlatformSyncListItemSchema, + DigitalOceanAppPlatformSyncSchema +} from "./digital-ocean-app-platform-sync-schemas"; + +export type TDigitalOceanAppPlatformSyncListItem = z.infer; + +export type TDigitalOceanAppPlatformSync = z.infer; + +export type TDigitalOceanAppPlatformSyncInput = z.infer; + +export type TDigitalOceanAppPlatformSyncWithCredentials = TDigitalOceanAppPlatformSync & { + connection: TDigitalOceanConnection; +}; + +export type TDigitalOceanAppPlatformSecret = TDigitalOceanVariable & { + type: "SECRET"; +}; diff --git a/backend/src/services/secret-sync/digital-ocean-app-platform/index.ts b/backend/src/services/secret-sync/digital-ocean-app-platform/index.ts new file mode 100644 index 000000000..9af720b23 --- /dev/null +++ b/backend/src/services/secret-sync/digital-ocean-app-platform/index.ts @@ -0,0 +1,4 @@ +export * from "./digital-ocean-app-platform-sync-constants"; +export * from "./digital-ocean-app-platform-sync-fns"; +export * from "./digital-ocean-app-platform-sync-schemas"; +export * from "./digital-ocean-app-platform-sync-types"; diff --git a/backend/src/services/secret-sync/secret-sync-enums.ts b/backend/src/services/secret-sync/secret-sync-enums.ts index 2c02e2ff4..55859200a 100644 --- a/backend/src/services/secret-sync/secret-sync-enums.ts +++ b/backend/src/services/secret-sync/secret-sync-enums.ts @@ -26,6 +26,7 @@ export enum SecretSync { Zabbix = "zabbix", Railway = "railway", Checkly = "checkly", + DigitalOceanAppPlatform = "digital-ocean-app-platform", Bitbucket = "bitbucket" } diff --git a/backend/src/services/secret-sync/secret-sync-fns.ts b/backend/src/services/secret-sync/secret-sync-fns.ts index 827a41b7d..caa6ddcee 100644 --- a/backend/src/services/secret-sync/secret-sync-fns.ts +++ b/backend/src/services/secret-sync/secret-sync-fns.ts @@ -35,6 +35,10 @@ import { ChecklySyncFns } from "./checkly/checkly-sync-fns"; import { CLOUDFLARE_PAGES_SYNC_LIST_OPTION } from "./cloudflare-pages/cloudflare-pages-constants"; import { CloudflarePagesSyncFns } from "./cloudflare-pages/cloudflare-pages-fns"; import { CLOUDFLARE_WORKERS_SYNC_LIST_OPTION, CloudflareWorkersSyncFns } from "./cloudflare-workers"; +import { + DIGITAL_OCEAN_APP_PLATFORM_SYNC_LIST_OPTION, + DigitalOceanAppPlatformSyncFns +} from "./digital-ocean-app-platform"; import { FLYIO_SYNC_LIST_OPTION, FlyioSyncFns } from "./flyio"; import { GCP_SYNC_LIST_OPTION } from "./gcp"; import { GcpSyncFns } from "./gcp/gcp-sync-fns"; @@ -82,6 +86,7 @@ const SECRET_SYNC_LIST_OPTIONS: Record = { [SecretSync.Zabbix]: ZABBIX_SYNC_LIST_OPTION, [SecretSync.Railway]: RAILWAY_SYNC_LIST_OPTION, [SecretSync.Checkly]: CHECKLY_SYNC_LIST_OPTION, + [SecretSync.DigitalOceanAppPlatform]: DIGITAL_OCEAN_APP_PLATFORM_SYNC_LIST_OPTION, [SecretSync.Bitbucket]: BITBUCKET_SYNC_LIST_OPTION }; @@ -260,6 +265,8 @@ export const SecretSyncFns = { return ChecklySyncFns.syncSecrets(secretSync, schemaSecretMap); case SecretSync.Supabase: return SupabaseSyncFns.syncSecrets(secretSync, schemaSecretMap); + case SecretSync.DigitalOceanAppPlatform: + return DigitalOceanAppPlatformSyncFns.syncSecrets(secretSync, schemaSecretMap); case SecretSync.Bitbucket: return BitbucketSyncFns.syncSecrets(secretSync, schemaSecretMap); default: @@ -369,6 +376,9 @@ export const SecretSyncFns = { case SecretSync.Supabase: secretMap = await SupabaseSyncFns.getSecrets(secretSync); break; + case SecretSync.DigitalOceanAppPlatform: + secretMap = await DigitalOceanAppPlatformSyncFns.getSecrets(secretSync); + break; case SecretSync.Bitbucket: secretMap = await BitbucketSyncFns.getSecrets(secretSync); break; @@ -459,6 +469,8 @@ export const SecretSyncFns = { return ChecklySyncFns.removeSecrets(secretSync, schemaSecretMap); case SecretSync.Supabase: return SupabaseSyncFns.removeSecrets(secretSync, schemaSecretMap); + case SecretSync.DigitalOceanAppPlatform: + return DigitalOceanAppPlatformSyncFns.removeSecrets(secretSync, schemaSecretMap); case SecretSync.Bitbucket: return BitbucketSyncFns.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 1de9838e3..2f2b3c427 100644 --- a/backend/src/services/secret-sync/secret-sync-maps.ts +++ b/backend/src/services/secret-sync/secret-sync-maps.ts @@ -29,6 +29,7 @@ export const SECRET_SYNC_NAME_MAP: Record = { [SecretSync.Zabbix]: "Zabbix", [SecretSync.Railway]: "Railway", [SecretSync.Checkly]: "Checkly", + [SecretSync.DigitalOceanAppPlatform]: "Digital Ocean App Platform", [SecretSync.Bitbucket]: "Bitbucket" }; @@ -60,6 +61,7 @@ export const SECRET_SYNC_CONNECTION_MAP: Record = { [SecretSync.Zabbix]: AppConnection.Zabbix, [SecretSync.Railway]: AppConnection.Railway, [SecretSync.Checkly]: AppConnection.Checkly, + [SecretSync.DigitalOceanAppPlatform]: AppConnection.DigitalOcean, [SecretSync.Bitbucket]: AppConnection.Bitbucket }; @@ -91,5 +93,6 @@ export const SECRET_SYNC_PLAN_MAP: Record = { [SecretSync.Zabbix]: SecretSyncPlanType.Regular, [SecretSync.Railway]: SecretSyncPlanType.Regular, [SecretSync.Checkly]: SecretSyncPlanType.Regular, + [SecretSync.DigitalOceanAppPlatform]: SecretSyncPlanType.Regular, [SecretSync.Bitbucket]: 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 007c79bf7..a844fbe48 100644 --- a/backend/src/services/secret-sync/secret-sync-types.ts +++ b/backend/src/services/secret-sync/secret-sync-types.ts @@ -96,6 +96,11 @@ import { TCloudflareWorkersSyncListItem, TCloudflareWorkersSyncWithCredentials } from "./cloudflare-workers"; +import { + TDigitalOceanAppPlatformSyncInput, + TDigitalOceanAppPlatformSyncListItem, + TDigitalOceanAppPlatformSyncWithCredentials +} from "./digital-ocean-app-platform/digital-ocean-app-platform-sync-types"; import { TFlyioSync, TFlyioSyncInput, TFlyioSyncListItem, TFlyioSyncWithCredentials } from "./flyio/flyio-sync-types"; import { TGcpSync, TGcpSyncInput, TGcpSyncListItem, TGcpSyncWithCredentials } from "./gcp"; import { TGitLabSync, TGitLabSyncInput, TGitLabSyncListItem, TGitLabSyncWithCredentials } from "./gitlab"; @@ -203,6 +208,7 @@ export type TSecretSyncWithCredentials = | TRailwaySyncWithCredentials | TChecklySyncWithCredentials | TSupabaseSyncWithCredentials + | TDigitalOceanAppPlatformSyncWithCredentials | TBitbucketSyncWithCredentials; export type TSecretSyncInput = @@ -233,6 +239,7 @@ export type TSecretSyncInput = | TRailwaySyncInput | TChecklySyncInput | TSupabaseSyncInput + | TDigitalOceanAppPlatformSyncInput | TBitbucketSyncInput; export type TSecretSyncListItem = @@ -263,6 +270,7 @@ export type TSecretSyncListItem = | TRailwaySyncListItem | TChecklySyncListItem | TSupabaseSyncListItem + | TDigitalOceanAppPlatformSyncListItem | TBitbucketSyncListItem; export type TSyncOptionsConfig = { diff --git a/docs/api-reference/endpoints/app-connections/digital-ocean/available.mdx b/docs/api-reference/endpoints/app-connections/digital-ocean/available.mdx new file mode 100644 index 000000000..78799ff30 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/digital-ocean/available.mdx @@ -0,0 +1,4 @@ +--- +title: "Available" +openapi: "GET /api/v1/app-connections/digital-ocean/available" +--- diff --git a/docs/api-reference/endpoints/app-connections/digital-ocean/create.mdx b/docs/api-reference/endpoints/app-connections/digital-ocean/create.mdx new file mode 100644 index 000000000..dc526fef7 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/digital-ocean/create.mdx @@ -0,0 +1,8 @@ +--- +title: "Create" +openapi: "POST /api/v1/app-connections/digital-ocean" +--- + + + Check out the configuration docs for [Digital Ocean Connections](/integrations/app-connections/digital-ocean) to learn how to obtain the required credentials. + diff --git a/docs/api-reference/endpoints/app-connections/digital-ocean/delete.mdx b/docs/api-reference/endpoints/app-connections/digital-ocean/delete.mdx new file mode 100644 index 000000000..0ec3daf3d --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/digital-ocean/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/digital-ocean/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/digital-ocean/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/digital-ocean/get-by-id.mdx new file mode 100644 index 000000000..522a88efc --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/digital-ocean/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/digital-ocean/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/digital-ocean/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/digital-ocean/get-by-name.mdx new file mode 100644 index 000000000..e950b6ef8 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/digital-ocean/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/digital-ocean/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/digital-ocean/list.mdx b/docs/api-reference/endpoints/app-connections/digital-ocean/list.mdx new file mode 100644 index 000000000..d49d1621a --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/digital-ocean/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/digital-ocean" +--- diff --git a/docs/api-reference/endpoints/app-connections/digital-ocean/update.mdx b/docs/api-reference/endpoints/app-connections/digital-ocean/update.mdx new file mode 100644 index 000000000..5ba755d67 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/digital-ocean/update.mdx @@ -0,0 +1,8 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/digital-ocean/{connectionId}" +--- + + + Check out the configuration docs for [Digital Ocean Connections](/integrations/app-connections/digital-ocean) to learn how to obtain the required credentials. + diff --git a/docs/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/create.mdx b/docs/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/create.mdx new file mode 100644 index 000000000..673fe7c0a --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-syncs/digital-ocean-app-platform" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/delete.mdx b/docs/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/delete.mdx new file mode 100644 index 000000000..79af0d463 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-syncs/digital-ocean-app-platform/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/get-by-id.mdx b/docs/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/get-by-id.mdx new file mode 100644 index 000000000..d8b9e9b68 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/secret-syncs/digital-ocean-app-platform/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/get-by-name.mdx b/docs/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/get-by-name.mdx new file mode 100644 index 000000000..82cf109e2 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/secret-syncs/digital-ocean-app-platform/sync-name/{syncName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/list.mdx b/docs/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/list.mdx new file mode 100644 index 000000000..e7c779906 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-syncs/digital-ocean-app-platform" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/remove-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/remove-secrets.mdx new file mode 100644 index 000000000..eee30d627 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/remove-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Secrets" +openapi: "POST /api/v1/secret-syncs/digital-ocean-app-platform/{syncId}/remove-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/sync-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/sync-secrets.mdx new file mode 100644 index 000000000..4d3e66c13 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/sync-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Secrets" +openapi: "POST /api/v1/secret-syncs/digital-ocean-app-platform/{syncId}/sync-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/update.mdx b/docs/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/update.mdx new file mode 100644 index 000000000..c6c8d45e1 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/secret-syncs/digital-ocean-app-platform/{syncId}" +--- diff --git a/docs/docs.json b/docs/docs.json index e8f6745c3..13ec5367e 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -476,6 +476,7 @@ "integrations/app-connections/checkly", "integrations/app-connections/cloudflare", "integrations/app-connections/databricks", + "integrations/app-connections/digital-ocean", "integrations/app-connections/flyio", "integrations/app-connections/gcp", "integrations/app-connections/github", @@ -522,6 +523,7 @@ "integrations/secret-syncs/cloudflare-pages", "integrations/secret-syncs/cloudflare-workers", "integrations/secret-syncs/databricks", + "integrations/secret-syncs/digital-ocean-app-platform", "integrations/secret-syncs/flyio", "integrations/secret-syncs/gcp-secret-manager", "integrations/secret-syncs/github", @@ -1382,6 +1384,18 @@ "api-reference/endpoints/app-connections/databricks/update", "api-reference/endpoints/app-connections/databricks/delete" ] + }, + { + "group": "Digital Ocean", + "pages": [ + "api-reference/endpoints/app-connections/digital-ocean/list", + "api-reference/endpoints/app-connections/digital-ocean/available", + "api-reference/endpoints/app-connections/digital-ocean/get-by-id", + "api-reference/endpoints/app-connections/digital-ocean/get-by-name", + "api-reference/endpoints/app-connections/digital-ocean/create", + "api-reference/endpoints/app-connections/digital-ocean/update", + "api-reference/endpoints/app-connections/digital-ocean/delete" + ] }, { "group": "Fly.io", @@ -1829,6 +1843,19 @@ "api-reference/endpoints/secret-syncs/databricks/remove-secrets" ] }, + { + "group": "Digital Ocean", + "pages": [ + "api-reference/endpoints/secret-syncs/digital-ocean-app-platform/list", + "api-reference/endpoints/secret-syncs/digital-ocean-app-platform/get-by-id", + "api-reference/endpoints/secret-syncs/digital-ocean-app-platform/get-by-name", + "api-reference/endpoints/secret-syncs/digital-ocean-app-platform/create", + "api-reference/endpoints/secret-syncs/digital-ocean-app-platform/update", + "api-reference/endpoints/secret-syncs/digital-ocean-app-platform/delete", + "api-reference/endpoints/secret-syncs/digital-ocean-app-platform/sync-secrets", + "api-reference/endpoints/secret-syncs/digital-ocean-app-platform/remove-secrets" + ] + }, { "group": "Fly.io", "pages": [ diff --git a/docs/images/app-connections/digital-ocean/app-connection-create-api-key.png b/docs/images/app-connections/digital-ocean/app-connection-create-api-key.png new file mode 100644 index 000000000..8b1bc20da Binary files /dev/null and b/docs/images/app-connections/digital-ocean/app-connection-create-api-key.png differ diff --git a/docs/images/app-connections/digital-ocean/app-connection-create-form-roles.png b/docs/images/app-connections/digital-ocean/app-connection-create-form-roles.png new file mode 100644 index 000000000..43033cf12 Binary files /dev/null and b/docs/images/app-connections/digital-ocean/app-connection-create-form-roles.png differ diff --git a/docs/images/app-connections/digital-ocean/app-connection-create-form.png b/docs/images/app-connections/digital-ocean/app-connection-create-form.png new file mode 100644 index 000000000..32fb7f9e4 Binary files /dev/null and b/docs/images/app-connections/digital-ocean/app-connection-create-form.png differ diff --git a/docs/images/app-connections/digital-ocean/app-connection-create-required-roles.png b/docs/images/app-connections/digital-ocean/app-connection-create-required-roles.png new file mode 100644 index 000000000..1fe865fb4 Binary files /dev/null and b/docs/images/app-connections/digital-ocean/app-connection-create-required-roles.png differ diff --git a/docs/images/app-connections/digital-ocean/app-connection-form.png b/docs/images/app-connections/digital-ocean/app-connection-form.png new file mode 100644 index 000000000..b96fb34d7 Binary files /dev/null and b/docs/images/app-connections/digital-ocean/app-connection-form.png differ diff --git a/docs/images/app-connections/digital-ocean/app-connection-generated.png b/docs/images/app-connections/digital-ocean/app-connection-generated.png new file mode 100644 index 000000000..0bda0e435 Binary files /dev/null and b/docs/images/app-connections/digital-ocean/app-connection-generated.png differ diff --git a/docs/images/app-connections/digital-ocean/app-connection-key-generated.png b/docs/images/app-connections/digital-ocean/app-connection-key-generated.png new file mode 100644 index 000000000..4ee42881e Binary files /dev/null and b/docs/images/app-connections/digital-ocean/app-connection-key-generated.png differ diff --git a/docs/images/app-connections/digital-ocean/app-connection-option.png b/docs/images/app-connections/digital-ocean/app-connection-option.png new file mode 100644 index 000000000..38336dc32 Binary files /dev/null and b/docs/images/app-connections/digital-ocean/app-connection-option.png differ diff --git a/docs/images/app-connections/digital-ocean/app-connection-profile.png b/docs/images/app-connections/digital-ocean/app-connection-profile.png new file mode 100644 index 000000000..a9cb9a1df Binary files /dev/null and b/docs/images/app-connections/digital-ocean/app-connection-profile.png differ diff --git a/docs/images/secret-syncs/digital-ocean-app-platform/select-option.png b/docs/images/secret-syncs/digital-ocean-app-platform/select-option.png new file mode 100644 index 000000000..f1709364f Binary files /dev/null and b/docs/images/secret-syncs/digital-ocean-app-platform/select-option.png differ diff --git a/docs/images/secret-syncs/digital-ocean-app-platform/sync-created.png b/docs/images/secret-syncs/digital-ocean-app-platform/sync-created.png new file mode 100644 index 000000000..4359de5f8 Binary files /dev/null and b/docs/images/secret-syncs/digital-ocean-app-platform/sync-created.png differ diff --git a/docs/images/secret-syncs/digital-ocean-app-platform/sync-destination.png b/docs/images/secret-syncs/digital-ocean-app-platform/sync-destination.png new file mode 100644 index 000000000..278cc7946 Binary files /dev/null and b/docs/images/secret-syncs/digital-ocean-app-platform/sync-destination.png differ diff --git a/docs/images/secret-syncs/digital-ocean-app-platform/sync-details.png b/docs/images/secret-syncs/digital-ocean-app-platform/sync-details.png new file mode 100644 index 000000000..d3d4d9c33 Binary files /dev/null and b/docs/images/secret-syncs/digital-ocean-app-platform/sync-details.png differ diff --git a/docs/images/secret-syncs/digital-ocean-app-platform/sync-options.png b/docs/images/secret-syncs/digital-ocean-app-platform/sync-options.png new file mode 100644 index 000000000..bd3137dbe Binary files /dev/null and b/docs/images/secret-syncs/digital-ocean-app-platform/sync-options.png differ diff --git a/docs/images/secret-syncs/digital-ocean-app-platform/sync-review.png b/docs/images/secret-syncs/digital-ocean-app-platform/sync-review.png new file mode 100644 index 000000000..e0e5f10bf Binary files /dev/null and b/docs/images/secret-syncs/digital-ocean-app-platform/sync-review.png differ diff --git a/docs/images/secret-syncs/digital-ocean-app-platform/sync-source.png b/docs/images/secret-syncs/digital-ocean-app-platform/sync-source.png new file mode 100644 index 000000000..1ce54d152 Binary files /dev/null and b/docs/images/secret-syncs/digital-ocean-app-platform/sync-source.png differ diff --git a/docs/integrations/app-connections/digital-ocean.mdx b/docs/integrations/app-connections/digital-ocean.mdx new file mode 100644 index 000000000..5b047f017 --- /dev/null +++ b/docs/integrations/app-connections/digital-ocean.mdx @@ -0,0 +1,113 @@ +--- +title: "DigitalOcean Connection" +description: "Learn how to configure a DigitalOcean Connection for Infisical." +--- + +Infisical supports the use of [API Tokens](https://cloud.digitalocean.com/account/api/tokens) to connect with DigitalOcean. + +## Create a DigitalOcean API Token + + + + ![DigitalOcean Dashboard](/images/app-connections/digital-ocean/app-connection-profile.png) + + + ![API Section](/images/app-connections/digital-ocean/app-connection-create-api-key.png) + + + Give your token a descriptive name and ensure custom scopes is selected. + + ![Token Form](/images/app-connections/digital-ocean/app-connection-create-form.png) + + + ``` + read:account + read:actions + read:regions + read:sizes + read:app/projects + update:app + ``` + + ![Token Form](/images/app-connections/digital-ocean/app-connection-create-form-roles.png) + ![Token Form](/images/app-connections/digital-ocean/app-connection-create-required-roles.png) + + + Make sure to copy the token now—you won't be able to see it again. + + ![Token Generated](/images/app-connections/digital-ocean/app-connection-key-generated.png) + + + +## Create a DigitalOcean Connection in Infisical + + + + + + In your Infisical dashboard, go to **Organization Settings** and open the [**App Connections**](https://app.infisical.com/organization/app-connections) tab. + + ![App Connections Tab](/images/app-connections/general/add-connection.png) + + + Click **+ Add Connection** and choose **DigitalOcean Connection** from the list of integrations. + + ![Select DigitalOcean Connection](/images/app-connections/digital-ocean/app-connection-option.png) + + + Complete the form by providing: + - A descriptive name for the connection + - An optional description + - The API Token from the previous step + + ![DigitalOcean Connection Modal](/images/app-connections/digital-ocean/app-connection-form.png) + + + After submitting the form, your **DigitalOcean Connection** will be successfully created and ready to use with your Infisical projects. + + ![DigitalOcean Connection Created](/images/app-connections/digital-ocean/app-connection-generated.png) + + + + + + To create a DigitalOcean Connection via API, send a request to the [Create DigitalOcean Connection](/api-reference/endpoints/app-connections/digital-ocean/create) endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/app-connections/digital-ocean \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-digitalocean-connection", + "method": "api-token", + "credentials": { + "apiToken": "[API TOKEN]" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "appConnection": { + "id": "a1b2c3d4-5678-90ab-cdef-1234567890ab", + "name": "my-digitalocean-connection", + "description": null, + "version": 1, + "orgId": "abcdef12-3456-7890-abcd-ef1234567890", + "createdAt": "2025-07-19T10:15:00.000Z", + "updatedAt": "2025-07-19T10:15:00.000Z", + "isPlatformManagedCredentials": false, + "credentialsHash": "d41d8cd98f00b204e9800998ecf8427e", + "app": "digital-ocean", + "method": "api-token", + "credentials": {} + } + } + ``` + + + diff --git a/docs/integrations/secret-syncs/digital-ocean-app-platform.mdx b/docs/integrations/secret-syncs/digital-ocean-app-platform.mdx new file mode 100644 index 000000000..91657c760 --- /dev/null +++ b/docs/integrations/secret-syncs/digital-ocean-app-platform.mdx @@ -0,0 +1,155 @@ +--- +title: "DigitalOcean App Platform Sync" +description: "Learn how to configure a DigitalOcean App Platform Sync for Infisical." +--- + +**Prerequisites:** + +- Create a [DigitalOcean Connection](/integrations/app-connections/digital-ocean) + + + + + + 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) + + + ![Select DigitalOcean](/images/secret-syncs/digital-ocean-app-platform/select-option.png) + + + Configure the **Source** from where secrets should be retrieved, then click **Next**. + + ![Configure Source](/images/secret-syncs/digital-ocean-app-platform/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). + + + + Configure the **Destination** to where secrets should be deployed, then click **Next**. + + ![Configure Destination](/images/secret-syncs/digital-ocean-app-platform/sync-destination.png) + + - **DigitalOcean Connection**: The DigitalOcean Connection to authenticate with. + - **App**: The App Platform app to sync secrets to. + + + Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. + + ![Configure Options](/images/secret-syncs/digital-ocean-app-platform/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. + + Digital Ocean App Platform does not support importing secrets. + + - **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. + + We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. + + - **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. + + + Configure the **Details** of your DigitalOcean Sync, then click **Next**. + + ![Configure Details](/images/secret-syncs/digital-ocean-app-platform/sync-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + + Review your DigitalOcean Sync configuration, then click **Create Sync**. + + ![Review Configuration](/images/secret-syncs/digital-ocean-app-platform/sync-review.png) + + + If enabled, your DigitalOcean Sync will begin syncing your secrets to the destination endpoint. + + ![Sync Created](/images/secret-syncs/digital-ocean-app-platform/sync-created.png) + + + + + + To create a **DigitalOcean App Platform Sync**, make an API request to the [Create DigitalOcean Sync](/api-reference/endpoints/secret-syncs/digital-ocean/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/secret-syncs/digital-ocean-app-platform \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-digitalocean-sync", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "sync to do app", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "environment": "dev", + "secretPath": "/app-secrets", + "isEnabled": true, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination", + "autoSyncEnabled": true, + "disableSecretDeletion": false + }, + "destinationConfig": { + "appId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "appName": "do-todo-app" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "secretSync": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-digitalocean-sync", + "description": "sync to do app", + "isEnabled": true, + "version": 1, + "folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2025-07-19T12:00:00Z", + "updatedAt": "2025-07-19T12:00:00Z", + "syncStatus": "succeeded", + "lastSyncJobId": "job-5678", + "lastSyncMessage": null, + "lastSyncedAt": "2025-07-19T12:00:00Z", + "syncOptions": { + "initialSyncBehavior": "overwrite-destination", + "autoSyncEnabled": true, + "disableSecretDeletion": false + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connection": { + "app": "digital-ocean", + "name": "my-digitalocean-connection", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "environment": { + "slug": "dev", + "name": "Development", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "folder": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "path": "/app-secrets" + }, + "destination": "digital-ocean", + "destinationConfig": { + "appId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "appName": "do-todo-app" + } + } + } + ``` + + diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/DigitalOceanAppPlatformSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/DigitalOceanAppPlatformSyncFields.tsx new file mode 100644 index 000000000..e50c82c3f --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/DigitalOceanAppPlatformSyncFields.tsx @@ -0,0 +1,63 @@ +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 } from "@app/components/v2"; +import { useDigitalOceanConnectionListApps } from "@app/hooks/api/appConnections/digital-ocean"; +import { TDigitalOceanApp } from "@app/hooks/api/appConnections/digital-ocean/types"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +import { TSecretSyncForm } from "../schemas"; + +export const DigitalOceanAppPlatformSyncFields = () => { + const { control, setValue } = useFormContext< + TSecretSyncForm & { destination: SecretSync.DigitalOceanAppPlatform } + >(); + + const connectionId = useWatch({ name: "connection.id", control }); + + const { data: apps = [], isPending: isAccountsLoading } = useDigitalOceanConnectionListApps( + connectionId, + { + enabled: Boolean(connectionId) + } + ); + + return ( + <> + { + setValue("destinationConfig.appId", ""); + setValue("destinationConfig.appName", ""); + }} + /> + ( + + p.id === value) ?? null} + onChange={(option) => { + const v = option as SingleValue; + onChange(v?.id ?? null); + setValue("destinationConfig.appName", v?.spec.name ?? ""); + }} + options={apps} + placeholder="Select an app..." + getOptionLabel={(option) => option.spec.name} + getOptionValue={(option) => option.id} + /> + + )} + /> + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx index c933fa44d..478447541 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx @@ -15,6 +15,7 @@ import { ChecklySyncFields } from "./ChecklySyncFields"; import { CloudflarePagesSyncFields } from "./CloudflarePagesSyncFields"; import { CloudflareWorkersSyncFields } from "./CloudflareWorkersSyncFields"; import { DatabricksSyncFields } from "./DatabricksSyncFields"; +import { DigitalOceanAppPlatformSyncFields } from "./DigitalOceanAppPlatformSyncFields"; import { FlyioSyncFields } from "./FlyioSyncFields"; import { GcpSyncFields } from "./GcpSyncFields"; import { GitHubSyncFields } from "./GitHubSyncFields"; @@ -92,6 +93,8 @@ export const SecretSyncDestinationFields = () => { return ; case SecretSync.Supabase: return ; + case SecretSync.DigitalOceanAppPlatform: + return ; case SecretSync.Bitbucket: 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 e7226eb68..50ea46ac0 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx @@ -63,6 +63,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => { case SecretSync.Railway: case SecretSync.Checkly: case SecretSync.Supabase: + case SecretSync.DigitalOceanAppPlatform: case SecretSync.Bitbucket: AdditionalSyncOptionsFieldsComponent = null; break; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/DigitalOceanAppPlatformSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/DigitalOceanAppPlatformSyncReviewFields.tsx new file mode 100644 index 000000000..e99d7c623 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/DigitalOceanAppPlatformSyncReviewFields.tsx @@ -0,0 +1,14 @@ +import { useFormContext } from "react-hook-form"; + +import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas"; +import { GenericFieldLabel } from "@app/components/v2"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +export const DigitalOceanAppPlatformSyncReviewFields = () => { + const { watch } = useFormContext< + TSecretSyncForm & { destination: SecretSync.DigitalOceanAppPlatform } + >(); + const appName = watch("destinationConfig.appName"); + + return {appName}; +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx index d785dc87c..c1194a4c1 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx @@ -24,6 +24,7 @@ import { ChecklySyncReviewFields } from "./ChecklySyncReviewFields"; import { CloudflarePagesSyncReviewFields } from "./CloudflarePagesReviewFields"; import { CloudflareWorkersSyncReviewFields } from "./CloudflareWorkersReviewFields"; import { DatabricksSyncReviewFields } from "./DatabricksSyncReviewFields"; +import { DigitalOceanAppPlatformSyncReviewFields } from "./DigitalOceanAppPlatformSyncReviewFields"; import { FlyioSyncReviewFields } from "./FlyioSyncReviewFields"; import { GcpSyncReviewFields } from "./GcpSyncReviewFields"; import { GitHubSyncReviewFields } from "./GitHubSyncReviewFields"; @@ -145,6 +146,9 @@ export const SecretSyncReviewFields = () => { case SecretSync.Supabase: DestinationFieldsComponent = ; break; + case SecretSync.DigitalOceanAppPlatform: + DestinationFieldsComponent = ; + break; case SecretSync.Bitbucket: DestinationFieldsComponent = ; break; diff --git a/frontend/src/components/secret-syncs/forms/schemas/digital-ocean-app-platform-sync-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/digital-ocean-app-platform-sync-destination-schema.ts new file mode 100644 index 000000000..2840f8b27 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/schemas/digital-ocean-app-platform-sync-destination-schema.ts @@ -0,0 +1,20 @@ +import { z } from "zod"; + +import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +export const DigitalOceanAppPlatformSyncDestinationSchema = BaseSecretSyncSchema().merge( + z.object({ + destination: z.literal(SecretSync.DigitalOceanAppPlatform), + destinationConfig: z.object({ + appId: z + .string() + .min(1, "App ID is required") + .max(255, "App ID must be less than 255 characters"), + appName: z + .string() + .min(1, "Account Name is required") + .max(255, "App Name must be less than 255 characters") + }) + }) +); 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 dc082e168..5ffd5940a 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 @@ -12,6 +12,7 @@ import { ChecklySyncDestinationSchema } from "./checkly-sync-destination-schema" import { CloudflarePagesSyncDestinationSchema } from "./cloudflare-pages-sync-destination-schema"; import { CloudflareWorkersSyncDestinationSchema } from "./cloudflare-workers-sync-destination-schema"; import { DatabricksSyncDestinationSchema } from "./databricks-sync-destination-schema"; +import { DigitalOceanAppPlatformSyncDestinationSchema } from "./digital-ocean-app-platform-sync-destination-schema"; import { FlyioSyncDestinationSchema } from "./flyio-sync-destination-schema"; import { GcpSyncDestinationSchema } from "./gcp-sync-destination-schema"; import { GitHubSyncDestinationSchema } from "./github-sync-destination-schema"; @@ -57,6 +58,7 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [ ZabbixSyncDestinationSchema, RailwaySyncDestinationSchema, ChecklySyncDestinationSchema, + DigitalOceanAppPlatformSyncDestinationSchema, BitbucketSyncDestinationSchema ]); diff --git a/frontend/src/helpers/appConnections.ts b/frontend/src/helpers/appConnections.ts index 807569c6e..1345cb493 100644 --- a/frontend/src/helpers/appConnections.ts +++ b/frontend/src/helpers/appConnections.ts @@ -17,9 +17,7 @@ import { AzureClientSecretsConnectionMethod, AzureDevOpsConnectionMethod, AzureKeyVaultConnectionMethod, - BitbucketConnectionMethod, CamundaConnectionMethod, - ChecklyConnectionMethod, CloudflareConnectionMethod, DatabricksConnectionMethod, FlyioConnectionMethod, @@ -28,19 +26,14 @@ import { GitHubRadarConnectionMethod, GitLabConnectionMethod, HCVaultConnectionMethod, - HerokuConnectionMethod, HumanitecConnectionMethod, LdapConnectionMethod, MsSqlConnectionMethod, MySqlConnectionMethod, - OCIConnectionMethod, OktaConnectionMethod, OnePassConnectionMethod, OracleDBConnectionMethod, PostgresConnectionMethod, - RailwayConnectionMethod, - RenderConnectionMethod, - SupabaseConnectionMethod, TAppConnection, TeamCityConnectionMethod, TerraformCloudConnectionMethod, @@ -48,6 +41,14 @@ import { WindmillConnectionMethod, ZabbixConnectionMethod } from "@app/hooks/api/appConnections/types"; +import { BitbucketConnectionMethod } from "@app/hooks/api/appConnections/types/bitbucket-connection"; +import { ChecklyConnectionMethod } from "@app/hooks/api/appConnections/types/checkly-connection"; +import { DigitalOceanConnectionMethod } from "@app/hooks/api/appConnections/types/digital-ocean"; +import { HerokuConnectionMethod } from "@app/hooks/api/appConnections/types/heroku-connection"; +import { OCIConnectionMethod } from "@app/hooks/api/appConnections/types/oci-connection"; +import { RailwayConnectionMethod } from "@app/hooks/api/appConnections/types/railway-connection"; +import { RenderConnectionMethod } from "@app/hooks/api/appConnections/types/render-connection"; +import { SupabaseConnectionMethod } from "@app/hooks/api/appConnections/types/supabase-connection"; export const APP_CONNECTION_MAP: Record< AppConnection, @@ -100,6 +101,10 @@ export const APP_CONNECTION_MAP: Record< [AppConnection.Bitbucket]: { name: "Bitbucket", image: "Bitbucket.png" }, [AppConnection.Checkly]: { name: "Checkly", image: "Checkly.png" }, [AppConnection.Supabase]: { name: "Supabase", image: "Supabase.png" }, + [AppConnection.DigitalOcean]: { + name: "Digital Ocean", + image: "Digital Ocean.png" + }, [AppConnection.Okta]: { name: "Okta", image: "Okta.png" } }; @@ -134,6 +139,7 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) case CloudflareConnectionMethod.ApiToken: case BitbucketConnectionMethod.ApiToken: case ZabbixConnectionMethod.ApiToken: + case DigitalOceanConnectionMethod.ApiToken: case OktaConnectionMethod.ApiToken: return { name: "API Token", icon: faKey }; case PostgresConnectionMethod.UsernameAndPassword: diff --git a/frontend/src/helpers/secretSyncs.ts b/frontend/src/helpers/secretSyncs.ts index cabd7fa6e..0eb41e119 100644 --- a/frontend/src/helpers/secretSyncs.ts +++ b/frontend/src/helpers/secretSyncs.ts @@ -102,6 +102,10 @@ export const SECRET_SYNC_MAP: Record = { [SecretSync.Zabbix]: AppConnection.Zabbix, [SecretSync.Railway]: AppConnection.Railway, [SecretSync.Checkly]: AppConnection.Checkly, + [SecretSync.DigitalOceanAppPlatform]: AppConnection.DigitalOcean, [SecretSync.Bitbucket]: AppConnection.Bitbucket }; diff --git a/frontend/src/hooks/api/appConnections/digital-ocean/index.ts b/frontend/src/hooks/api/appConnections/digital-ocean/index.ts new file mode 100644 index 000000000..b69c25120 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/digital-ocean/index.ts @@ -0,0 +1 @@ +export * from "./queries"; diff --git a/frontend/src/hooks/api/appConnections/digital-ocean/queries.ts b/frontend/src/hooks/api/appConnections/digital-ocean/queries.ts new file mode 100644 index 000000000..7b75fb819 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/digital-ocean/queries.ts @@ -0,0 +1,37 @@ +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; +import { appConnectionKeys } from "@app/hooks/api/appConnections"; + +import { TDigitalOceanApp } from "./types"; + +const digitalOceanAppPlatformConnectionKeys = { + all: [...appConnectionKeys.all, "digitalOceanAppPlatform"] as const, + listAccounts: (connectionId: string) => + [...digitalOceanAppPlatformConnectionKeys.all, "workspace-scopes", connectionId] as const +}; + +export const useDigitalOceanConnectionListApps = ( + connectionId: string, + options?: Omit< + UseQueryOptions< + TDigitalOceanApp[], + unknown, + TDigitalOceanApp[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: digitalOceanAppPlatformConnectionKeys.listAccounts(connectionId), + queryFn: async () => { + const { data } = await apiRequest.get<{ apps: TDigitalOceanApp[] }>( + `/api/v1/app-connections/digital-ocean/${connectionId}/apps` + ); + + return data.apps; + }, + ...options + }); +}; diff --git a/frontend/src/hooks/api/appConnections/digital-ocean/types.ts b/frontend/src/hooks/api/appConnections/digital-ocean/types.ts new file mode 100644 index 000000000..065730dfa --- /dev/null +++ b/frontend/src/hooks/api/appConnections/digital-ocean/types.ts @@ -0,0 +1,16 @@ +export type TDigitalOceanAppPlatformVariable = { + key: string; + value: string; + type: "SECRET" | "GENERAL"; +}; + +export type TDigitalOceanApp = { + id: string; + spec: { + name: string; + services: Array<{ + name: string; + }>; + envs?: TDigitalOceanAppPlatformVariable[]; + }; +}; diff --git a/frontend/src/hooks/api/appConnections/enums.ts b/frontend/src/hooks/api/appConnections/enums.ts index 2e1e59655..c3a1fec05 100644 --- a/frontend/src/hooks/api/appConnections/enums.ts +++ b/frontend/src/hooks/api/appConnections/enums.ts @@ -33,5 +33,6 @@ export enum AppConnection { Railway = "railway", Checkly = "checkly", Supabase = "supabase", + DigitalOcean = "digital-ocean", Okta = "okta" } diff --git a/frontend/src/hooks/api/appConnections/types/app-options.ts b/frontend/src/hooks/api/appConnections/types/app-options.ts index e9bc5b243..7dc5907e3 100644 --- a/frontend/src/hooks/api/appConnections/types/app-options.ts +++ b/frontend/src/hooks/api/appConnections/types/app-options.ts @@ -152,6 +152,10 @@ export type TSupabaseConnectionOption = TAppConnectionOptionBase & { app: AppConnection.Supabase; }; +export type TDigitalOceanConnectionOption = TAppConnectionOptionBase & { + app: AppConnection.DigitalOcean; +}; + export type TOktaConnectionOption = TAppConnectionOptionBase & { app: AppConnection.Okta; }; @@ -188,6 +192,7 @@ export type TAppConnectionOption = | TZabbixConnectionOption | TRailwayConnectionOption | TChecklyConnectionOption + | TDigitalOceanConnectionOption | TOktaConnectionOption; export type TAppConnectionOptionMap = { @@ -225,5 +230,6 @@ export type TAppConnectionOptionMap = { [AppConnection.Railway]: TRailwayConnectionOption; [AppConnection.Checkly]: TChecklyConnectionOption; [AppConnection.Supabase]: TSupabaseConnectionOption; + [AppConnection.DigitalOcean]: TDigitalOceanConnectionOption; [AppConnection.Okta]: TOktaConnectionOption; }; diff --git a/frontend/src/hooks/api/appConnections/types/digital-ocean.ts b/frontend/src/hooks/api/appConnections/types/digital-ocean.ts new file mode 100644 index 000000000..52b7dcfe9 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/types/digital-ocean.ts @@ -0,0 +1,14 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection"; + +export enum DigitalOceanConnectionMethod { + ApiToken = "api-token" +} + +export type TDigitalOceanConnection = TRootAppConnection & { + app: AppConnection.DigitalOcean; + method: DigitalOceanConnectionMethod.ApiToken; + credentials: { + apiToken: string; + }; +}; diff --git a/frontend/src/hooks/api/appConnections/types/index.ts b/frontend/src/hooks/api/appConnections/types/index.ts index 5085feab3..2924c3009 100644 --- a/frontend/src/hooks/api/appConnections/types/index.ts +++ b/frontend/src/hooks/api/appConnections/types/index.ts @@ -12,6 +12,7 @@ import { TCamundaConnection } from "./camunda-connection"; import { TChecklyConnection } from "./checkly-connection"; import { TCloudflareConnection } from "./cloudflare-connection"; import { TDatabricksConnection } from "./databricks-connection"; +import { TDigitalOceanConnection } from "./digital-ocean"; import { TFlyioConnection } from "./flyio-connection"; import { TGcpConnection } from "./gcp-connection"; import { TGitHubConnection } from "./github-connection"; @@ -107,6 +108,7 @@ export type TAppConnection = | TRailwayConnection | TChecklyConnection | TSupabaseConnection + | TDigitalOceanConnection | TOktaConnection; export type TAvailableAppConnection = Pick; @@ -178,5 +180,6 @@ export type TAppConnectionMap = { [AppConnection.Railway]: TRailwayConnection; [AppConnection.Checkly]: TChecklyConnection; [AppConnection.Supabase]: TSupabaseConnection; + [AppConnection.DigitalOcean]: TDigitalOceanConnection; [AppConnection.Okta]: TOktaConnection; }; diff --git a/frontend/src/hooks/api/secretSyncs/enums.ts b/frontend/src/hooks/api/secretSyncs/enums.ts index f29599a08..11de8cd62 100644 --- a/frontend/src/hooks/api/secretSyncs/enums.ts +++ b/frontend/src/hooks/api/secretSyncs/enums.ts @@ -26,6 +26,7 @@ export enum SecretSync { Zabbix = "zabbix", Railway = "railway", Checkly = "checkly", + DigitalOceanAppPlatform = "digital-ocean-app-platform", Bitbucket = "bitbucket" } diff --git a/frontend/src/hooks/api/secretSyncs/types/digital-ocean-app-platform-sync.ts b/frontend/src/hooks/api/secretSyncs/types/digital-ocean-app-platform-sync.ts new file mode 100644 index 000000000..98d152531 --- /dev/null +++ b/frontend/src/hooks/api/secretSyncs/types/digital-ocean-app-platform-sync.ts @@ -0,0 +1,17 @@ +/* eslint-disable @typescript-eslint/no-empty-object-type */ +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 TDigitalOceanAppPlatformSync = TRootSecretSync & { + destination: SecretSync.DigitalOceanAppPlatform; + destinationConfig: { + appId: string; + appName: string; + }; + connection: { + app: AppConnection.DigitalOcean; + name: string; + id: string; + }; +}; diff --git a/frontend/src/hooks/api/secretSyncs/types/index.ts b/frontend/src/hooks/api/secretSyncs/types/index.ts index daffac56a..7af01765e 100644 --- a/frontend/src/hooks/api/secretSyncs/types/index.ts +++ b/frontend/src/hooks/api/secretSyncs/types/index.ts @@ -14,6 +14,7 @@ import { TChecklySync } from "./checkly-sync"; import { TCloudflarePagesSync } from "./cloudflare-pages-sync"; import { TCloudflareWorkersSync } from "./cloudflare-workers-sync"; import { TDatabricksSync } from "./databricks-sync"; +import { TDigitalOceanAppPlatformSync } from "./digital-ocean-app-platform-sync"; import { TFlyioSync } from "./flyio-sync"; import { TGcpSync } from "./gcp-sync"; import { TGitHubSync } from "./github-sync"; @@ -65,6 +66,7 @@ export type TSecretSync = | TRailwaySync | TChecklySync | TSupabaseSync + | TDigitalOceanAppPlatformSync | TBitbucketSync; 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 b1181a68b..fa8c85b66 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx @@ -21,6 +21,7 @@ import { CamundaConnectionForm } from "./CamundaConnectionForm"; import { ChecklyConnectionForm } from "./ChecklyConnectionForm"; import { CloudflareConnectionForm } from "./CloudflareConnectionForm"; import { DatabricksConnectionForm } from "./DatabricksConnectionForm"; +import { DigitalOceanConnectionForm } from "./DigitalOceanConnectionForm"; import { FlyioConnectionForm } from "./FlyioConnectionForm"; import { GcpConnectionForm } from "./GcpConnectionForm"; import { GitHubConnectionForm } from "./GitHubConnectionForm"; @@ -150,6 +151,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => { return ; case AppConnection.Supabase: return ; + case AppConnection.DigitalOcean: + return ; case AppConnection.Okta: return ; default: @@ -256,6 +259,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => { return ; case AppConnection.Supabase: return ; + case AppConnection.DigitalOcean: + return ; case AppConnection.Okta: return ; default: diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/DigitalOceanConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/DigitalOceanConnectionForm.tsx new file mode 100644 index 000000000..7663bd522 --- /dev/null +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/DigitalOceanConnectionForm.tsx @@ -0,0 +1,136 @@ +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 { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { + DigitalOceanConnectionMethod, + TDigitalOceanConnection +} from "@app/hooks/api/appConnections/types/digital-ocean"; + +import { + genericAppConnectionFieldsSchema, + GenericAppConnectionsFields +} from "./GenericAppConnectionFields"; + +type Props = { + appConnection?: TDigitalOceanConnection; + onSubmit: (formData: FormData) => void; +}; + +const rootSchema = genericAppConnectionFieldsSchema.extend({ + app: z.literal(AppConnection.DigitalOcean) +}); + +const formSchema = z.discriminatedUnion("method", [ + rootSchema.extend({ + method: z.literal(DigitalOceanConnectionMethod.ApiToken), + credentials: z.object({ + apiToken: z.string().trim().min(1, "API Token required") + }) + }) +]); + +type FormData = z.infer; + +export const DigitalOceanConnectionForm = ({ appConnection, onSubmit }: Props) => { + const isUpdate = Boolean(appConnection); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: appConnection ?? { + app: AppConnection.DigitalOcean, + method: DigitalOceanConnectionMethod.ApiToken + } + }); + + const { + handleSubmit, + control, + formState: { isSubmitting, isDirty } + } = form; + + return ( + +
+ {!isUpdate && } + ( + + + + )} + /> + ( + + onChange(e.target.value)} + /> + + )} + /> + +
+ + + + +
+ +
+ ); +}; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/DigitalOceanAppPlatformSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/DigitalOceanAppPlatformSyncDestinationCol.tsx new file mode 100644 index 000000000..0dbf13ff8 --- /dev/null +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/DigitalOceanAppPlatformSyncDestinationCol.tsx @@ -0,0 +1,14 @@ +import { TDigitalOceanAppPlatformSync } from "@app/hooks/api/secretSyncs/types/digital-ocean-app-platform-sync"; + +import { getSecretSyncDestinationColValues } from "../helpers"; +import { SecretSyncTableCell } from "../SecretSyncTableCell"; + +type Props = { + secretSync: TDigitalOceanAppPlatformSync; +}; + +export const DigitalOceanAppPlatformSyncDestinationCol = ({ secretSync }: Props) => { + const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync); + + 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 10ffa9e8c..f6848f748 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 @@ -12,6 +12,7 @@ import { ChecklySyncDestinationCol } from "./ChecklySyncDestinationCol"; import { CloudflarePagesSyncDestinationCol } from "./CloudflarePagesSyncDestinationCol"; import { CloudflareWorkersSyncDestinationCol } from "./CloudflareWorkersSyncDestinationCol"; import { DatabricksSyncDestinationCol } from "./DatabricksSyncDestinationCol"; +import { DigitalOceanAppPlatformSyncDestinationCol } from "./DigitalOceanAppPlatformSyncDestinationCol"; import { FlyioSyncDestinationCol } from "./FlyioSyncDestinationCol"; import { GcpSyncDestinationCol } from "./GcpSyncDestinationCol"; import { GitHubSyncDestinationCol } from "./GitHubSyncDestinationCol"; @@ -89,6 +90,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => { return ; case SecretSync.Supabase: return ; + case SecretSync.DigitalOceanAppPlatform: + return ; case SecretSync.Bitbucket: 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 e8d95344b..1ee3b141e 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 @@ -174,6 +174,10 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => { primaryText = destinationConfig.projectName; secondaryText = "Supabase Project"; break; + case SecretSync.DigitalOceanAppPlatform: + primaryText = destinationConfig.appName; + secondaryText = "Digital Ocean App"; + break; case SecretSync.Bitbucket: primaryText = destinationConfig.workspaceSlug; secondaryText = destinationConfig.repositorySlug; diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/DigitalOceanAppPlatformSyncDestinationSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/DigitalOceanAppPlatformSyncDestinationSection.tsx new file mode 100644 index 000000000..f69231217 --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/DigitalOceanAppPlatformSyncDestinationSection.tsx @@ -0,0 +1,12 @@ +import { GenericFieldLabel } from "@app/components/secret-syncs"; +import { TDigitalOceanAppPlatformSync } from "@app/hooks/api/secretSyncs/types/digital-ocean-app-platform-sync"; + +type Props = { + secretSync: TDigitalOceanAppPlatformSync; +}; + +export const DigitalOceanAppPlatformSyncDestinationSection = ({ secretSync }: Props) => { + const { destinationConfig } = secretSync; + + return {destinationConfig.appName}; +}; 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 44220c200..6213b264d 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx @@ -23,6 +23,7 @@ import { ChecklySyncDestinationSection } from "./ChecklySyncDestinationSection"; import { CloudflarePagesSyncDestinationSection } from "./CloudflarePagesSyncDestinationSection"; import { CloudflareWorkersSyncDestinationSection } from "./CloudflareWorkersSyncDestinationSection"; import { DatabricksSyncDestinationSection } from "./DatabricksSyncDestinationSection"; +import { DigitalOceanAppPlatformSyncDestinationSection } from "./DigitalOceanAppPlatformSyncDestinationSection"; import { FlyioSyncDestinationSection } from "./FlyioSyncDestinationSection"; import { GcpSyncDestinationSection } from "./GcpSyncDestinationSection"; import { GitHubSyncDestinationSection } from "./GitHubSyncDestinationSection"; @@ -135,6 +136,11 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }: case SecretSync.Supabase: DestinationComponents = ; break; + case SecretSync.DigitalOceanAppPlatform: + DestinationComponents = ( + + ); + break; case SecretSync.Bitbucket: 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 eb366952d..843e02f63 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx @@ -65,6 +65,7 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) = case SecretSync.Railway: case SecretSync.Supabase: case SecretSync.Checkly: + case SecretSync.DigitalOceanAppPlatform: case SecretSync.Bitbucket: AdditionalSyncOptionsComponent = null; break;