diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index c0cbdfe8a..7c79cb54e 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -2225,6 +2225,9 @@ export const AppConnections = { ONEPASS: { instanceUrl: "The URL of the 1Password Connect Server instance to authenticate with.", apiToken: "The API token used to access the 1Password Connect Server." + }, + FLYIO: { + accessToken: "The Access Token used to access fly.io." } } }; @@ -2386,6 +2389,9 @@ export const SecretSyncs = { }, ONEPASS: { vaultId: "The ID of the 1Password vault to sync secrets to." + }, + FLYIO: { + appId: "The ID of the Fly.io app to sync secrets to." } } }; diff --git a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts index 53c503e4a..3a9af21c7 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 @@ -39,6 +39,7 @@ import { DatabricksConnectionListItemSchema, SanitizedDatabricksConnectionSchema } from "@app/services/app-connection/databricks"; +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"; import { @@ -100,7 +101,8 @@ const SanitizedAppConnectionSchema = z.union([ ...SanitizedTeamCityConnectionSchema.options, ...SanitizedOCIConnectionSchema.options, ...SanitizedOracleDBConnectionSchema.options, - ...SanitizedOnePassConnectionSchema.options + ...SanitizedOnePassConnectionSchema.options, + ...SanitizedFlyioConnectionSchema.options ]); const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ @@ -127,7 +129,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ TeamCityConnectionListItemSchema, OCIConnectionListItemSchema, OracleDBConnectionListItemSchema, - OnePassConnectionListItemSchema + OnePassConnectionListItemSchema, + FlyioConnectionListItemSchema ]); export const registerAppConnectionRouter = async (server: FastifyZodProvider) => { diff --git a/backend/src/server/routes/v1/app-connection-routers/flyio-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/flyio-connection-router.ts new file mode 100644 index 000000000..c2a1199aa --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/flyio-connection-router.ts @@ -0,0 +1,51 @@ +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 { + CreateFlyioConnectionSchema, + SanitizedFlyioConnectionSchema, + UpdateFlyioConnectionSchema +} from "@app/services/app-connection/flyio"; +import { AuthMode } from "@app/services/auth/auth-type"; + +import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; + +export const registerFlyioConnectionRouter = async (server: FastifyZodProvider) => { + registerAppConnectionEndpoints({ + app: AppConnection.Flyio, + server, + sanitizedResponseSchema: SanitizedFlyioConnectionSchema, + createSchema: CreateFlyioConnectionSchema, + updateSchema: UpdateFlyioConnectionSchema + }); + + // The following endpoints are for internal Infisical App use only and not part of the public API + server.route({ + method: "GET", + url: `/:connectionId/apps`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + response: { + 200: z + .object({ + id: z.string(), + name: z.string() + }) + .array() + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { connectionId } = req.params; + const apps = await server.services.appConnection.flyio.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 5f07f1be7..7c8df632c 100644 --- a/backend/src/server/routes/v1/app-connection-routers/index.ts +++ b/backend/src/server/routes/v1/app-connection-routers/index.ts @@ -11,6 +11,7 @@ import { registerAzureDevOpsConnectionRouter } from "./azure-devops-connection-r import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connection-router"; import { registerCamundaConnectionRouter } from "./camunda-connection-router"; import { registerDatabricksConnectionRouter } from "./databricks-connection-router"; +import { registerFlyioConnectionRouter } from "./flyio-connection-router"; import { registerGcpConnectionRouter } from "./gcp-connection-router"; import { registerGitHubConnectionRouter } from "./github-connection-router"; import { registerGitHubRadarConnectionRouter } from "./github-radar-connection-router"; @@ -52,5 +53,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record + registerSyncSecretsEndpoints({ + destination: SecretSync.Flyio, + server, + responseSchema: FlyioSyncSchema, + createSchema: CreateFlyioSyncSchema, + updateSchema: UpdateFlyioSyncSchema + }); 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 3f4276726..39d6747a4 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/index.ts +++ b/backend/src/server/routes/v1/secret-sync-routers/index.ts @@ -9,6 +9,7 @@ import { registerAzureDevOpsSyncRouter } from "./azure-devops-sync-router"; import { registerAzureKeyVaultSyncRouter } from "./azure-key-vault-sync-router"; import { registerCamundaSyncRouter } from "./camunda-sync-router"; import { registerDatabricksSyncRouter } from "./databricks-sync-router"; +import { registerFlyioSyncRouter } from "./flyio-sync-router"; import { registerGcpSyncRouter } from "./gcp-sync-router"; import { registerGitHubSyncRouter } from "./github-sync-router"; import { registerHCVaultSyncRouter } from "./hc-vault-sync-router"; @@ -37,5 +38,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record { diff --git a/backend/src/services/app-connection/app-connection-enums.ts b/backend/src/services/app-connection/app-connection-enums.ts index 19aabc55e..d7c03fd3f 100644 --- a/backend/src/services/app-connection/app-connection-enums.ts +++ b/backend/src/services/app-connection/app-connection-enums.ts @@ -22,7 +22,8 @@ export enum AppConnection { TeamCity = "teamcity", OCI = "oci", OracleDB = "oracledb", - OnePass = "1password" + OnePass = "1password", + Flyio = "flyio" } export enum AWSRegion { diff --git a/backend/src/services/app-connection/app-connection-fns.ts b/backend/src/services/app-connection/app-connection-fns.ts index 4a272bb5c..1d9bf0be9 100644 --- a/backend/src/services/app-connection/app-connection-fns.ts +++ b/backend/src/services/app-connection/app-connection-fns.ts @@ -56,6 +56,7 @@ import { getDatabricksConnectionListItem, validateDatabricksConnectionCredentials } from "./databricks"; +import { FlyioConnectionMethod, getFlyioConnectionListItem, validateFlyioConnectionCredentials } from "./flyio"; import { GcpConnectionMethod, getGcpConnectionListItem, validateGcpConnectionCredentials } from "./gcp"; import { getGitHubConnectionListItem, GitHubConnectionMethod, validateGitHubConnectionCredentials } from "./github"; import { @@ -121,7 +122,8 @@ export const listAppConnectionOptions = () => { getTeamCityConnectionListItem(), getOCIConnectionListItem(), getOracleDBConnectionListItem(), - getOnePassConnectionListItem() + getOnePassConnectionListItem(), + getFlyioConnectionListItem() ].sort((a, b) => a.name.localeCompare(b.name)); }; @@ -196,7 +198,8 @@ export const validateAppConnectionCredentials = async ( [AppConnection.TeamCity]: validateTeamCityConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.OCI]: validateOCIConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.OracleDB]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator, - [AppConnection.OnePass]: validateOnePassConnectionCredentials as TAppConnectionCredentialsValidator + [AppConnection.OnePass]: validateOnePassConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.Flyio]: validateFlyioConnectionCredentials as TAppConnectionCredentialsValidator }; return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection); @@ -238,6 +241,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) => case HCVaultConnectionMethod.AccessToken: case TeamCityConnectionMethod.AccessToken: case AzureDevOpsConnectionMethod.AccessToken: + case FlyioConnectionMethod.AccessToken: return "Access Token"; case Auth0ConnectionMethod.ClientCredentials: return "Client Credentials"; @@ -299,7 +303,8 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record< [AppConnection.TeamCity]: platformManagedCredentialsNotSupported, [AppConnection.OCI]: platformManagedCredentialsNotSupported, [AppConnection.OracleDB]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform, - [AppConnection.OnePass]: platformManagedCredentialsNotSupported + [AppConnection.OnePass]: platformManagedCredentialsNotSupported, + [AppConnection.Flyio]: platformManagedCredentialsNotSupported }; export const enterpriseAppCheck = async ( diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts index 19967aa3b..7080d7053 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -24,7 +24,8 @@ export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.TeamCity]: "TeamCity", [AppConnection.OCI]: "OCI", [AppConnection.OracleDB]: "OracleDB", - [AppConnection.OnePass]: "1Password" + [AppConnection.OnePass]: "1Password", + [AppConnection.Flyio]: "Fly.io" }; export const APP_CONNECTION_PLAN_MAP: Record = { @@ -51,5 +52,6 @@ export const APP_CONNECTION_PLAN_MAP: Record>>; @@ -192,6 +199,7 @@ export type TAppConnectionInput = { id: string } & ( | TOCIConnectionInput | TOracleDBConnectionInput | TOnePassConnectionInput + | TFlyioConnectionInput ); export type TSqlConnectionInput = @@ -230,7 +238,8 @@ export type TAppConnectionConfig = | TLdapConnectionConfig | TTeamCityConnectionConfig | TOCIConnectionConfig - | TOnePassConnectionConfig; + | TOnePassConnectionConfig + | TFlyioConnectionConfig; export type TValidateAppConnectionCredentialsSchema = | TValidateAwsConnectionCredentialsSchema @@ -256,7 +265,8 @@ export type TValidateAppConnectionCredentialsSchema = | TValidateTeamCityConnectionCredentialsSchema | TValidateOCIConnectionCredentialsSchema | TValidateOracleDBConnectionCredentialsSchema - | TValidateOnePassConnectionCredentialsSchema; + | TValidateOnePassConnectionCredentialsSchema + | TValidateFlyioConnectionCredentialsSchema; export type TListAwsConnectionKmsKeys = { connectionId: string; diff --git a/backend/src/services/app-connection/flyio/flyio-connection-enums.ts b/backend/src/services/app-connection/flyio/flyio-connection-enums.ts new file mode 100644 index 000000000..651e46658 --- /dev/null +++ b/backend/src/services/app-connection/flyio/flyio-connection-enums.ts @@ -0,0 +1,3 @@ +export enum FlyioConnectionMethod { + AccessToken = "access-token" +} diff --git a/backend/src/services/app-connection/flyio/flyio-connection-fns.ts b/backend/src/services/app-connection/flyio/flyio-connection-fns.ts new file mode 100644 index 000000000..56d4c0b31 --- /dev/null +++ b/backend/src/services/app-connection/flyio/flyio-connection-fns.ts @@ -0,0 +1,72 @@ +import { AxiosError } from "axios"; + +import { request } from "@app/lib/config/request"; +import { BadRequestError } from "@app/lib/errors"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; + +import { FlyioConnectionMethod } from "./flyio-connection-enums"; +import { TFlyioApp, TFlyioConnection, TFlyioConnectionConfig } from "./flyio-connection-types"; + +export const getFlyioConnectionListItem = () => { + return { + name: "Fly.io" as const, + app: AppConnection.Flyio as const, + methods: Object.values(FlyioConnectionMethod) as [FlyioConnectionMethod.AccessToken] + }; +}; + +export const validateFlyioConnectionCredentials = async (config: TFlyioConnectionConfig) => { + const { accessToken } = config.credentials; + + try { + const resp = await request.post<{ data: { viewer: { id: string | null; email: string } } | null }>( + IntegrationUrls.FLYIO_API_URL, + { query: "query { viewer { id email } }" }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json" + } + } + ); + + if (resp.data.data === null) { + throw new BadRequestError({ + message: "Unable to validate connection: Invalid access token provided." + }); + } + } 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; +}; + +export const listFlyioApps = async (appConnection: TFlyioConnection) => { + const { accessToken } = appConnection.credentials; + + const resp = await request.post<{ data: { apps: { nodes: TFlyioApp[] } } }>( + IntegrationUrls.FLYIO_API_URL, + { + query: + "query GetApps { apps { nodes { id name hostname status organization { id slug } currentRelease { version status createdAt } } } }" + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + Accept: "application/json" + } + } + ); + + return resp.data.data.apps.nodes; +}; diff --git a/backend/src/services/app-connection/flyio/flyio-connection-schemas.ts b/backend/src/services/app-connection/flyio/flyio-connection-schemas.ts new file mode 100644 index 000000000..4466d24df --- /dev/null +++ b/backend/src/services/app-connection/flyio/flyio-connection-schemas.ts @@ -0,0 +1,62 @@ +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 { FlyioConnectionMethod } from "./flyio-connection-enums"; + +export const FlyioConnectionAccessTokenCredentialsSchema = z.object({ + accessToken: z + .string() + .trim() + .min(1, "Access Token required") + .max(1000) + .startsWith("FlyV1", "Token must start with 'FlyV1'") + .describe(AppConnections.CREDENTIALS.FLYIO.accessToken) +}); + +const BaseFlyioConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Flyio) }); + +export const FlyioConnectionSchema = BaseFlyioConnectionSchema.extend({ + method: z.literal(FlyioConnectionMethod.AccessToken), + credentials: FlyioConnectionAccessTokenCredentialsSchema +}); + +export const SanitizedFlyioConnectionSchema = z.discriminatedUnion("method", [ + BaseFlyioConnectionSchema.extend({ + method: z.literal(FlyioConnectionMethod.AccessToken), + credentials: FlyioConnectionAccessTokenCredentialsSchema.pick({}) + }) +]); + +export const ValidateFlyioConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: z.literal(FlyioConnectionMethod.AccessToken).describe(AppConnections.CREATE(AppConnection.Flyio).method), + credentials: FlyioConnectionAccessTokenCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.Flyio).credentials + ) + }) +]); + +export const CreateFlyioConnectionSchema = ValidateFlyioConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.Flyio) +); + +export const UpdateFlyioConnectionSchema = z + .object({ + credentials: FlyioConnectionAccessTokenCredentialsSchema.optional().describe( + AppConnections.UPDATE(AppConnection.Flyio).credentials + ) + }) + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Flyio)); + +export const FlyioConnectionListItemSchema = z.object({ + name: z.literal("Fly.io"), + app: z.literal(AppConnection.Flyio), + methods: z.nativeEnum(FlyioConnectionMethod).array() +}); diff --git a/backend/src/services/app-connection/flyio/flyio-connection-service.ts b/backend/src/services/app-connection/flyio/flyio-connection-service.ts new file mode 100644 index 000000000..dd88633a0 --- /dev/null +++ b/backend/src/services/app-connection/flyio/flyio-connection-service.ts @@ -0,0 +1,30 @@ +import { logger } from "@app/lib/logger"; +import { OrgServiceActor } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { listFlyioApps } from "./flyio-connection-fns"; +import { TFlyioConnection } from "./flyio-connection-types"; + +type TGetAppConnectionFunc = ( + app: AppConnection, + connectionId: string, + actor: OrgServiceActor +) => Promise; + +export const flyioConnectionService = (getAppConnection: TGetAppConnectionFunc) => { + const listApps = async (connectionId: string, actor: OrgServiceActor) => { + const appConnection = await getAppConnection(AppConnection.Flyio, connectionId, actor); + + try { + const apps = await listFlyioApps(appConnection); + return apps; + } catch (error) { + logger.error(error, "Failed to establish connection with fly.io"); + return []; + } + }; + + return { + listApps + }; +}; diff --git a/backend/src/services/app-connection/flyio/flyio-connection-types.ts b/backend/src/services/app-connection/flyio/flyio-connection-types.ts new file mode 100644 index 000000000..f643caffb --- /dev/null +++ b/backend/src/services/app-connection/flyio/flyio-connection-types.ts @@ -0,0 +1,27 @@ +import z from "zod"; + +import { DiscriminativePick } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { + CreateFlyioConnectionSchema, + FlyioConnectionSchema, + ValidateFlyioConnectionCredentialsSchema +} from "./flyio-connection-schemas"; + +export type TFlyioConnection = z.infer; + +export type TFlyioConnectionInput = z.infer & { + app: AppConnection.Flyio; +}; + +export type TValidateFlyioConnectionCredentialsSchema = typeof ValidateFlyioConnectionCredentialsSchema; + +export type TFlyioConnectionConfig = DiscriminativePick & { + orgId: string; +}; + +export type TFlyioApp = { + id: string; + name: string; +}; diff --git a/backend/src/services/app-connection/flyio/index.ts b/backend/src/services/app-connection/flyio/index.ts new file mode 100644 index 000000000..f6aefd6b1 --- /dev/null +++ b/backend/src/services/app-connection/flyio/index.ts @@ -0,0 +1,4 @@ +export * from "./flyio-connection-enums"; +export * from "./flyio-connection-fns"; +export * from "./flyio-connection-schemas"; +export * from "./flyio-connection-types"; diff --git a/backend/src/services/secret-sync/flyio/flyio-sync-constants.ts b/backend/src/services/secret-sync/flyio/flyio-sync-constants.ts new file mode 100644 index 000000000..7e9e6b287 --- /dev/null +++ b/backend/src/services/secret-sync/flyio/flyio-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 FLYIO_SYNC_LIST_OPTION: TSecretSyncListItem = { + name: "Fly.io", + destination: SecretSync.Flyio, + connection: AppConnection.Flyio, + canImportSecrets: false +}; diff --git a/backend/src/services/secret-sync/flyio/flyio-sync-fns.ts b/backend/src/services/secret-sync/flyio/flyio-sync-fns.ts new file mode 100644 index 000000000..7c006fb64 --- /dev/null +++ b/backend/src/services/secret-sync/flyio/flyio-sync-fns.ts @@ -0,0 +1,133 @@ +import { request } from "@app/lib/config/request"; +import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; +import { + TDeleteFlyioVariable, + TFlyioListVariables, + TFlyioSecret, + TFlyioSyncWithCredentials, + TPutFlyioVariable +} from "@app/services/secret-sync/flyio/flyio-sync-types"; +import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; +import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns"; +import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; + +import { SECRET_SYNC_NAME_MAP } from "../secret-sync-maps"; + +const listFlyioSecrets = async ({ accessToken, appId }: TFlyioListVariables) => { + const { data } = await request.post<{ data: { app: { secrets: TFlyioSecret[] } } }>( + IntegrationUrls.FLYIO_API_URL, + { + query: "query GetAppSecrets($appId: String!) { app(id: $appId) { id name secrets { name createdAt } } }", + variables: { appId } + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + Accept: "application/json" + } + } + ); + + return data.data.app.secrets.map((s) => s.name); +}; + +const putFlyioSecrets = async ({ accessToken, appId, secretMap }: TPutFlyioVariable) => { + return request.post( + IntegrationUrls.FLYIO_API_URL, + { + query: + "mutation SetAppSecrets($appId: ID!, $secrets: [SecretInput!]!) { setSecrets(input: { appId: $appId, secrets: $secrets }) { app { name } release { version } } }", + variables: { + appId, + secrets: Object.entries(secretMap).map(([key, { value }]) => ({ key, value })) + } + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json" + } + } + ); +}; + +const deleteFlyioSecrets = async ({ accessToken, appId, keys }: TDeleteFlyioVariable) => { + return request.post( + IntegrationUrls.FLYIO_API_URL, + { + query: + "mutation UnsetAppSecrets($appId: ID!, $keys: [String!]!) { unsetSecrets(input: { appId: $appId, keys: $keys }) { app { name } release { version } } }", + variables: { + appId, + keys + } + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json" + } + } + ); +}; + +export const FlyioSyncFns = { + syncSecrets: async (secretSync: TFlyioSyncWithCredentials, secretMap: TSecretMap) => { + const { + connection, + environment, + destinationConfig: { appId } + } = secretSync; + + const { accessToken } = connection.credentials; + + try { + await putFlyioSecrets({ accessToken, appId, secretMap }); + } catch (error) { + throw new SecretSyncError({ + error + }); + } + + if (secretSync.syncOptions.disableSecretDeletion) return; + + const secrets = await listFlyioSecrets({ accessToken, appId }); + + const keys = secrets.filter( + (secret) => + matchesSchema(secret, environment?.slug || "", secretSync.syncOptions.keySchema) && !(secret in secretMap) + ); + + try { + await deleteFlyioSecrets({ accessToken, appId, keys }); + } catch (error) { + throw new SecretSyncError({ + error + }); + } + }, + removeSecrets: async (secretSync: TFlyioSyncWithCredentials, secretMap: TSecretMap) => { + const { + connection, + destinationConfig: { appId } + } = secretSync; + + const { accessToken } = connection.credentials; + + const secrets = await listFlyioSecrets({ accessToken, appId }); + + const keys = secrets.filter((secret) => secret in secretMap); + + try { + await deleteFlyioSecrets({ accessToken, appId, keys }); + } catch (error) { + throw new SecretSyncError({ + error + }); + } + }, + getSecrets: async (secretSync: TFlyioSyncWithCredentials) => { + throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`); + } +}; diff --git a/backend/src/services/secret-sync/flyio/flyio-sync-schemas.ts b/backend/src/services/secret-sync/flyio/flyio-sync-schemas.ts new file mode 100644 index 000000000..b353f94b4 --- /dev/null +++ b/backend/src/services/secret-sync/flyio/flyio-sync-schemas.ts @@ -0,0 +1,43 @@ +import { z } from "zod"; + +import { SecretSyncs } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { + BaseSecretSyncSchema, + GenericCreateSecretSyncFieldsSchema, + GenericUpdateSecretSyncFieldsSchema +} from "@app/services/secret-sync/secret-sync-schemas"; +import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; + +const FlyioSyncDestinationConfigSchema = z.object({ + appId: z.string().trim().min(1, "App required").max(255).describe(SecretSyncs.DESTINATION_CONFIG.FLYIO.appId) +}); + +const FlyioSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false }; + +export const FlyioSyncSchema = BaseSecretSyncSchema(SecretSync.Flyio, FlyioSyncOptionsConfig).extend({ + destination: z.literal(SecretSync.Flyio), + destinationConfig: FlyioSyncDestinationConfigSchema +}); + +export const CreateFlyioSyncSchema = GenericCreateSecretSyncFieldsSchema( + SecretSync.Flyio, + FlyioSyncOptionsConfig +).extend({ + destinationConfig: FlyioSyncDestinationConfigSchema +}); + +export const UpdateFlyioSyncSchema = GenericUpdateSecretSyncFieldsSchema( + SecretSync.Flyio, + FlyioSyncOptionsConfig +).extend({ + destinationConfig: FlyioSyncDestinationConfigSchema.optional() +}); + +export const FlyioSyncListItemSchema = z.object({ + name: z.literal("Fly.io"), + connection: z.literal(AppConnection.Flyio), + destination: z.literal(SecretSync.Flyio), + canImportSecrets: z.literal(false) +}); diff --git a/backend/src/services/secret-sync/flyio/flyio-sync-types.ts b/backend/src/services/secret-sync/flyio/flyio-sync-types.ts new file mode 100644 index 000000000..336639091 --- /dev/null +++ b/backend/src/services/secret-sync/flyio/flyio-sync-types.ts @@ -0,0 +1,32 @@ +import { z } from "zod"; + +import { TFlyioConnection } from "@app/services/app-connection/flyio"; + +import { CreateFlyioSyncSchema, FlyioSyncListItemSchema, FlyioSyncSchema } from "./flyio-sync-schemas"; + +export type TFlyioSync = z.infer; + +export type TFlyioSyncInput = z.infer; + +export type TFlyioSyncListItem = z.infer; + +export type TFlyioSyncWithCredentials = TFlyioSync & { + connection: TFlyioConnection; +}; + +export type TFlyioSecret = { + name: string; +}; + +export type TFlyioListVariables = { + accessToken: string; + appId: string; +}; + +export type TPutFlyioVariable = TFlyioListVariables & { + secretMap: { [key: string]: { value: string } }; +}; + +export type TDeleteFlyioVariable = TFlyioListVariables & { + keys: string[]; +}; diff --git a/backend/src/services/secret-sync/flyio/index.ts b/backend/src/services/secret-sync/flyio/index.ts new file mode 100644 index 000000000..b2fe2b0d5 --- /dev/null +++ b/backend/src/services/secret-sync/flyio/index.ts @@ -0,0 +1,4 @@ +export * from "./flyio-sync-constants"; +export * from "./flyio-sync-fns"; +export * from "./flyio-sync-schemas"; +export * from "./flyio-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 3cf940459..85e458feb 100644 --- a/backend/src/services/secret-sync/secret-sync-enums.ts +++ b/backend/src/services/secret-sync/secret-sync-enums.ts @@ -15,7 +15,8 @@ export enum SecretSync { HCVault = "hashicorp-vault", TeamCity = "teamcity", OCIVault = "oci-vault", - OnePass = "1password" + OnePass = "1password", + Flyio = "flyio" } export enum SecretSyncInitialSyncBehavior { diff --git a/backend/src/services/secret-sync/secret-sync-fns.ts b/backend/src/services/secret-sync/secret-sync-fns.ts index bcda32967..30589785e 100644 --- a/backend/src/services/secret-sync/secret-sync-fns.ts +++ b/backend/src/services/secret-sync/secret-sync-fns.ts @@ -29,6 +29,7 @@ import { AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION, azureAppConfigurationSyncFact import { AZURE_DEVOPS_SYNC_LIST_OPTION, azureDevOpsSyncFactory } from "./azure-devops"; import { AZURE_KEY_VAULT_SYNC_LIST_OPTION, azureKeyVaultSyncFactory } from "./azure-key-vault"; import { CAMUNDA_SYNC_LIST_OPTION, camundaSyncFactory } from "./camunda"; +import { FLYIO_SYNC_LIST_OPTION, FlyioSyncFns } from "./flyio"; import { GCP_SYNC_LIST_OPTION } from "./gcp"; import { GcpSyncFns } from "./gcp/gcp-sync-fns"; import { HC_VAULT_SYNC_LIST_OPTION, HCVaultSyncFns } from "./hc-vault"; @@ -57,7 +58,8 @@ const SECRET_SYNC_LIST_OPTIONS: Record = { [SecretSync.HCVault]: HC_VAULT_SYNC_LIST_OPTION, [SecretSync.TeamCity]: TEAMCITY_SYNC_LIST_OPTION, [SecretSync.OCIVault]: OCI_VAULT_SYNC_LIST_OPTION, - [SecretSync.OnePass]: ONEPASS_SYNC_LIST_OPTION + [SecretSync.OnePass]: ONEPASS_SYNC_LIST_OPTION, + [SecretSync.Flyio]: FLYIO_SYNC_LIST_OPTION }; export const listSecretSyncOptions = () => { @@ -215,6 +217,8 @@ export const SecretSyncFns = { return OCIVaultSyncFns.syncSecrets(secretSync, schemaSecretMap); case SecretSync.OnePass: return OnePassSyncFns.syncSecrets(secretSync, schemaSecretMap); + case SecretSync.Flyio: + return FlyioSyncFns.syncSecrets(secretSync, schemaSecretMap); default: throw new Error( `Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` @@ -292,6 +296,9 @@ export const SecretSyncFns = { case SecretSync.OnePass: secretMap = await OnePassSyncFns.getSecrets(secretSync); break; + case SecretSync.Flyio: + secretMap = await FlyioSyncFns.getSecrets(secretSync); + break; default: throw new Error( `Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` @@ -359,6 +366,8 @@ export const SecretSyncFns = { return OCIVaultSyncFns.removeSecrets(secretSync, schemaSecretMap); case SecretSync.OnePass: return OnePassSyncFns.removeSecrets(secretSync, schemaSecretMap); + case SecretSync.Flyio: + return FlyioSyncFns.removeSecrets(secretSync, schemaSecretMap); default: throw new Error( `Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` diff --git a/backend/src/services/secret-sync/secret-sync-maps.ts b/backend/src/services/secret-sync/secret-sync-maps.ts index dd329734a..4e2f50b70 100644 --- a/backend/src/services/secret-sync/secret-sync-maps.ts +++ b/backend/src/services/secret-sync/secret-sync-maps.ts @@ -18,7 +18,8 @@ export const SECRET_SYNC_NAME_MAP: Record = { [SecretSync.HCVault]: "Hashicorp Vault", [SecretSync.TeamCity]: "TeamCity", [SecretSync.OCIVault]: "OCI Vault", - [SecretSync.OnePass]: "1Password" + [SecretSync.OnePass]: "1Password", + [SecretSync.Flyio]: "Fly.io" }; export const SECRET_SYNC_CONNECTION_MAP: Record = { @@ -38,7 +39,8 @@ export const SECRET_SYNC_CONNECTION_MAP: Record = { [SecretSync.HCVault]: AppConnection.HCVault, [SecretSync.TeamCity]: AppConnection.TeamCity, [SecretSync.OCIVault]: AppConnection.OCI, - [SecretSync.OnePass]: AppConnection.OnePass + [SecretSync.OnePass]: AppConnection.OnePass, + [SecretSync.Flyio]: AppConnection.Flyio }; export const SECRET_SYNC_PLAN_MAP: Record = { @@ -58,5 +60,6 @@ export const SECRET_SYNC_PLAN_MAP: Record = { [SecretSync.HCVault]: SecretSyncPlanType.Regular, [SecretSync.TeamCity]: SecretSyncPlanType.Regular, [SecretSync.OCIVault]: SecretSyncPlanType.Enterprise, - [SecretSync.OnePass]: SecretSyncPlanType.Regular + [SecretSync.OnePass]: SecretSyncPlanType.Regular, + [SecretSync.Flyio]: SecretSyncPlanType.Regular }; diff --git a/backend/src/services/secret-sync/secret-sync-types.ts b/backend/src/services/secret-sync/secret-sync-types.ts index ff355e5e1..192140db5 100644 --- a/backend/src/services/secret-sync/secret-sync-types.ts +++ b/backend/src/services/secret-sync/secret-sync-types.ts @@ -72,6 +72,7 @@ import { TAzureKeyVaultSyncListItem, TAzureKeyVaultSyncWithCredentials } from "./azure-key-vault"; +import { TFlyioSync, TFlyioSyncInput, TFlyioSyncListItem, TFlyioSyncWithCredentials } from "./flyio/flyio-sync-types"; import { TGcpSync, TGcpSyncInput, TGcpSyncListItem, TGcpSyncWithCredentials } from "./gcp"; import { THCVaultSync, @@ -116,7 +117,8 @@ export type TSecretSync = | THCVaultSync | TTeamCitySync | TOCIVaultSync - | TOnePassSync; + | TOnePassSync + | TFlyioSync; export type TSecretSyncWithCredentials = | TAwsParameterStoreSyncWithCredentials @@ -135,7 +137,8 @@ export type TSecretSyncWithCredentials = | THCVaultSyncWithCredentials | TTeamCitySyncWithCredentials | TOCIVaultSyncWithCredentials - | TOnePassSyncWithCredentials; + | TOnePassSyncWithCredentials + | TFlyioSyncWithCredentials; export type TSecretSyncInput = | TAwsParameterStoreSyncInput @@ -154,7 +157,8 @@ export type TSecretSyncInput = | THCVaultSyncInput | TTeamCitySyncInput | TOCIVaultSyncInput - | TOnePassSyncInput; + | TOnePassSyncInput + | TFlyioSyncInput; export type TSecretSyncListItem = | TAwsParameterStoreSyncListItem @@ -173,7 +177,8 @@ export type TSecretSyncListItem = | THCVaultSyncListItem | TTeamCitySyncListItem | TOCIVaultSyncListItem - | TOnePassSyncListItem; + | TOnePassSyncListItem + | TFlyioSyncListItem; export type TSyncOptionsConfig = { canImportSecrets: boolean; diff --git a/docs/api-reference/endpoints/app-connections/flyio/available.mdx b/docs/api-reference/endpoints/app-connections/flyio/available.mdx new file mode 100644 index 000000000..20a643942 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/flyio/available.mdx @@ -0,0 +1,4 @@ +--- +title: "Available" +openapi: "GET /api/v1/app-connections/flyio/available" +--- diff --git a/docs/api-reference/endpoints/app-connections/flyio/create.mdx b/docs/api-reference/endpoints/app-connections/flyio/create.mdx new file mode 100644 index 000000000..afe00ba5e --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/flyio/create.mdx @@ -0,0 +1,8 @@ +--- +title: "Create" +openapi: "POST /api/v1/app-connections/flyio" +--- + + + Check out the configuration docs for [Fly.io Connections](/integrations/app-connections/flyio) to learn how to obtain the required credentials. + diff --git a/docs/api-reference/endpoints/app-connections/flyio/delete.mdx b/docs/api-reference/endpoints/app-connections/flyio/delete.mdx new file mode 100644 index 000000000..ec5907840 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/flyio/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/flyio/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/flyio/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/flyio/get-by-id.mdx new file mode 100644 index 000000000..ad09af5c6 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/flyio/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/flyio/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/flyio/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/flyio/get-by-name.mdx new file mode 100644 index 000000000..f6053d02a --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/flyio/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/flyio/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/flyio/list.mdx b/docs/api-reference/endpoints/app-connections/flyio/list.mdx new file mode 100644 index 000000000..73271db2e --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/flyio/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/flyio" +--- diff --git a/docs/api-reference/endpoints/app-connections/flyio/update.mdx b/docs/api-reference/endpoints/app-connections/flyio/update.mdx new file mode 100644 index 000000000..0cc6e2496 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/flyio/update.mdx @@ -0,0 +1,8 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/flyio/{connectionId}" +--- + + + Check out the configuration docs for [Fly.io Connections](/integrations/app-connections/flyio) to learn how to obtain the required credentials. + diff --git a/docs/api-reference/endpoints/secret-syncs/flyio/create.mdx b/docs/api-reference/endpoints/secret-syncs/flyio/create.mdx new file mode 100644 index 000000000..a080e09c4 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/flyio/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-syncs/flyio" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/flyio/delete.mdx b/docs/api-reference/endpoints/secret-syncs/flyio/delete.mdx new file mode 100644 index 000000000..5dbd5ff1b --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/flyio/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-syncs/flyio/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/flyio/get-by-id.mdx b/docs/api-reference/endpoints/secret-syncs/flyio/get-by-id.mdx new file mode 100644 index 000000000..8dfff7915 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/flyio/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/secret-syncs/flyio/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/flyio/get-by-name.mdx b/docs/api-reference/endpoints/secret-syncs/flyio/get-by-name.mdx new file mode 100644 index 000000000..0121546b0 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/flyio/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/secret-syncs/flyio/sync-name/{syncName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/flyio/import-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/flyio/import-secrets.mdx new file mode 100644 index 000000000..39f3475fb --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/flyio/import-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Import Secrets" +openapi: "POST /api/v1/secret-syncs/flyio/{syncId}/import-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/flyio/list.mdx b/docs/api-reference/endpoints/secret-syncs/flyio/list.mdx new file mode 100644 index 000000000..784c78cac --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/flyio/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-syncs/flyio" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/flyio/remove-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/flyio/remove-secrets.mdx new file mode 100644 index 000000000..3159dd51a --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/flyio/remove-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Secrets" +openapi: "POST /api/v1/secret-syncs/flyio/{syncId}/remove-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/flyio/sync-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/flyio/sync-secrets.mdx new file mode 100644 index 000000000..3495b9ae4 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/flyio/sync-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Secrets" +openapi: "POST /api/v1/secret-syncs/flyio/{syncId}/sync-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/flyio/update.mdx b/docs/api-reference/endpoints/secret-syncs/flyio/update.mdx new file mode 100644 index 000000000..cf448327f --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/flyio/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/secret-syncs/flyio/{syncId}" +--- diff --git a/docs/images/app-connections/flyio/app-connection-created.png b/docs/images/app-connections/flyio/app-connection-created.png new file mode 100644 index 000000000..befffbd02 Binary files /dev/null and b/docs/images/app-connections/flyio/app-connection-created.png differ diff --git a/docs/images/app-connections/flyio/app-connection-modal.png b/docs/images/app-connections/flyio/app-connection-modal.png new file mode 100644 index 000000000..3bf372241 Binary files /dev/null and b/docs/images/app-connections/flyio/app-connection-modal.png differ diff --git a/docs/images/app-connections/flyio/app-connection-option.png b/docs/images/app-connections/flyio/app-connection-option.png new file mode 100644 index 000000000..056ca6b76 Binary files /dev/null and b/docs/images/app-connections/flyio/app-connection-option.png differ diff --git a/docs/images/app-connections/flyio/create-token-page.png b/docs/images/app-connections/flyio/create-token-page.png new file mode 100644 index 000000000..910a6af58 Binary files /dev/null and b/docs/images/app-connections/flyio/create-token-page.png differ diff --git a/docs/images/app-connections/flyio/create-token.png b/docs/images/app-connections/flyio/create-token.png new file mode 100644 index 000000000..3dab26411 Binary files /dev/null and b/docs/images/app-connections/flyio/create-token.png differ diff --git a/docs/images/app-connections/flyio/dashboard-page.png b/docs/images/app-connections/flyio/dashboard-page.png new file mode 100644 index 000000000..ff567f1e2 Binary files /dev/null and b/docs/images/app-connections/flyio/dashboard-page.png differ diff --git a/docs/images/secret-syncs/flyio/configure-destination.png b/docs/images/secret-syncs/flyio/configure-destination.png new file mode 100644 index 000000000..d11a194ad Binary files /dev/null and b/docs/images/secret-syncs/flyio/configure-destination.png differ diff --git a/docs/images/secret-syncs/flyio/configure-details.png b/docs/images/secret-syncs/flyio/configure-details.png new file mode 100644 index 000000000..d0f70a3f3 Binary files /dev/null and b/docs/images/secret-syncs/flyio/configure-details.png differ diff --git a/docs/images/secret-syncs/flyio/configure-source.png b/docs/images/secret-syncs/flyio/configure-source.png new file mode 100644 index 000000000..75507f6fe Binary files /dev/null and b/docs/images/secret-syncs/flyio/configure-source.png differ diff --git a/docs/images/secret-syncs/flyio/configure-sync-options.png b/docs/images/secret-syncs/flyio/configure-sync-options.png new file mode 100644 index 000000000..f62e433e4 Binary files /dev/null and b/docs/images/secret-syncs/flyio/configure-sync-options.png differ diff --git a/docs/images/secret-syncs/flyio/review-configuration.png b/docs/images/secret-syncs/flyio/review-configuration.png new file mode 100644 index 000000000..a76ead15b Binary files /dev/null and b/docs/images/secret-syncs/flyio/review-configuration.png differ diff --git a/docs/images/secret-syncs/flyio/select-option.png b/docs/images/secret-syncs/flyio/select-option.png new file mode 100644 index 000000000..ab5be088a Binary files /dev/null and b/docs/images/secret-syncs/flyio/select-option.png differ diff --git a/docs/images/secret-syncs/flyio/sync-created.png b/docs/images/secret-syncs/flyio/sync-created.png new file mode 100644 index 000000000..30a23f0d9 Binary files /dev/null and b/docs/images/secret-syncs/flyio/sync-created.png differ diff --git a/docs/integrations/app-connections/flyio.mdx b/docs/integrations/app-connections/flyio.mdx new file mode 100644 index 000000000..e42756254 --- /dev/null +++ b/docs/integrations/app-connections/flyio.mdx @@ -0,0 +1,96 @@ +--- +title: "Fly.io Connection" +description: "Learn how to configure a Fly.io Connection for Infisical." +--- + +Infisical supports the use of [Access Tokens](https://fly.io/docs/security/tokens/) to connect with Fly.io. + +## Create Fly.io Access Token + + + + ![Dashboard Page](/images/app-connections/flyio/dashboard-page.png) + + + ![Click Create Token](/images/app-connections/flyio/create-token.png) + + + Ensure that you give this token access to the correct app, then click 'Create Token'. + + ![Create Token Page](/images/app-connections/flyio/create-token-page.png) + + + After clicking 'Create Token', a modal containing your access token will appear. Save this token for later steps. + + + +## Create Fly.io Connection in Infisical + + + + + + In your Infisical dashboard, go to **Organization Settings** and select the [**App Connections**](https://app.infisical.com/organization/app-connections) tab. + + ![App Connections Tab](/images/app-connections/general/add-connection.png) + + + Click the **+ Add Connection** button and select the **Fly.io Connection** option from the available integrations. + + ![Select Fly.io Connection](/images/app-connections/flyio/app-connection-option.png) + + + Complete the Fly.io Connection form by entering: + - A descriptive name for the connection + - An optional description for future reference + - The Access Token from earlier steps + + ![Fly.io Connection Modal](/images/app-connections/flyio/app-connection-modal.png) + + + After clicking Create, your **Fly.io Connection** is established and ready to use with your Infisical projects. + + ![Fly.io Connection Created](/images/app-connections/flyio/app-connection-created.png) + + + + + To create a Fly.io Connection, make an API request to the [Create Fly.io Connection](/api-reference/endpoints/app-connections/flyio/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/app-connections/flyio \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-flyio-connection", + "method": "access-token", + "credentials": { + "accessToken": "[PRIVATE TOKEN]" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "appConnection": { + "id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6", + "name": "my-flyio-connection", + "description": null, + "version": 1, + "orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c", + "createdAt": "2025-04-23T19:46:34.831Z", + "updatedAt": "2025-04-23T19:46:34.831Z", + "isPlatformManagedCredentials": false, + "credentialsHash": "7c2d371dec195f82a6a0d5b41c970a229cfcaf88e894a5b6395e2dbd0280661f", + "app": "flyio", + "method": "access-token", + "credentials": {} + } + } + ``` + + diff --git a/docs/integrations/secret-syncs/flyio.mdx b/docs/integrations/secret-syncs/flyio.mdx new file mode 100644 index 000000000..adc8a32d0 --- /dev/null +++ b/docs/integrations/secret-syncs/flyio.mdx @@ -0,0 +1,155 @@ +--- +title: "Fly.io Sync" +description: "Learn how to configure a Fly.io Sync for Infisical." +--- + +**Prerequisites:** +- Create a [Fly.io Connection](/integrations/app-connections/flyio) + + + + + + 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 Fly.io](/images/secret-syncs/flyio/select-option.png) + + + Configure the **Source** from where secrets should be retrieved, then click **Next**. + + ![Configure Source](/images/secret-syncs/flyio/configure-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/flyio/configure-destination.png) + + - **Fly.io Connection**: The Fly.io Connection to authenticate with. + - **App**: The Fly.io app to sync secrets to. + + + Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. + + ![Configure Sync Options](/images/secret-syncs/flyio/configure-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. + + Fly.io 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 Fly.io Sync, then click **Next**. + + ![Configure Details](/images/secret-syncs/flyio/configure-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + + Review your Fly.io Sync configuration, then click **Create Sync**. + + ![Review Configuration](/images/secret-syncs/flyio/review-configuration.png) + + + If enabled, your Fly.io Sync will begin syncing your secrets to the destination endpoint. + + ![Sync Created](/images/secret-syncs/flyio/sync-created.png) + + + + + To create a **Fly.io Sync**, make an API request to the [Create Fly.io Sync](/api-reference/endpoints/secret-syncs/flyio/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/secret-syncs/flyio \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-flyio-sync", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "an example sync", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "environment": "dev", + "secretPath": "/my-secrets", + "isEnabled": true, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "destinationConfig": { + "appId": "..." + } + }' + ``` + + ### Sample response + + ```bash Response + { + "secretSync": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-flyio-sync", + "description": "an example sync", + "isEnabled": true, + "version": 1, + "folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "syncStatus": "succeeded", + "lastSyncJobId": "123", + "lastSyncMessage": null, + "lastSyncedAt": "2023-11-07T05:31:56Z", + "importStatus": null, + "lastImportJobId": null, + "lastImportMessage": null, + "lastImportedAt": null, + "removeStatus": null, + "lastRemoveJobId": null, + "lastRemoveMessage": null, + "lastRemovedAt": null, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connection": { + "app": "flyio", + "name": "my-flyio-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": "/my-secrets" + }, + "destination": "flyio", + "destinationConfig": { + "appId": "..." + } + } + } + ``` + + diff --git a/docs/mint.json b/docs/mint.json index 28281a0ef..fee9a6dc7 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -504,6 +504,7 @@ "integrations/app-connections/azure-key-vault", "integrations/app-connections/camunda", "integrations/app-connections/databricks", + "integrations/app-connections/flyio", "integrations/app-connections/gcp", "integrations/app-connections/github", "integrations/app-connections/github-radar", @@ -538,6 +539,7 @@ "integrations/secret-syncs/azure-key-vault", "integrations/secret-syncs/camunda", "integrations/secret-syncs/databricks", + "integrations/secret-syncs/flyio", "integrations/secret-syncs/gcp-secret-manager", "integrations/secret-syncs/github", "integrations/secret-syncs/hashicorp-vault", @@ -1263,6 +1265,18 @@ "api-reference/endpoints/app-connections/databricks/delete" ] }, + { + "group": "Fly.io", + "pages": [ + "api-reference/endpoints/app-connections/flyio/list", + "api-reference/endpoints/app-connections/flyio/available", + "api-reference/endpoints/app-connections/flyio/get-by-id", + "api-reference/endpoints/app-connections/flyio/get-by-name", + "api-reference/endpoints/app-connections/flyio/create", + "api-reference/endpoints/app-connections/flyio/update", + "api-reference/endpoints/app-connections/flyio/delete" + ] + }, { "group": "GCP", "pages": [ @@ -1560,6 +1574,19 @@ "api-reference/endpoints/secret-syncs/databricks/remove-secrets" ] }, + { + "group": "Fly.io", + "pages": [ + "api-reference/endpoints/secret-syncs/flyio/list", + "api-reference/endpoints/secret-syncs/flyio/get-by-id", + "api-reference/endpoints/secret-syncs/flyio/get-by-name", + "api-reference/endpoints/secret-syncs/flyio/create", + "api-reference/endpoints/secret-syncs/flyio/update", + "api-reference/endpoints/secret-syncs/flyio/delete", + "api-reference/endpoints/secret-syncs/flyio/sync-secrets", + "api-reference/endpoints/secret-syncs/flyio/remove-secrets" + ] + }, { "group": "GCP Secret Manager", "pages": [ diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/FlyioSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/FlyioSyncFields.tsx new file mode 100644 index 000000000..a2a5bf844 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/FlyioSyncFields.tsx @@ -0,0 +1,51 @@ +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 { TFlyioApp, useFlyioConnectionListApps } from "@app/hooks/api/appConnections/flyio"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +import { TSecretSyncForm } from "../schemas"; + +export const FlyioSyncFields = () => { + const { control, setValue } = useFormContext< + TSecretSyncForm & { destination: SecretSync.Flyio } + >(); + + const connectionId = useWatch({ name: "connection.id", control }); + + const { data: apps, isLoading: isAppsLoading } = useFlyioConnectionListApps(connectionId, { + enabled: Boolean(connectionId) + }); + + return ( + <> + { + setValue("destinationConfig.appId", ""); + }} + /> + + ( + + v.id === value) ?? null} + onChange={(option) => onChange((option as SingleValue)?.id ?? null)} + options={apps} + placeholder="Select an app..." + getOptionLabel={(option) => option.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 71405c448..37976301a 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx @@ -11,6 +11,7 @@ import { AzureDevOpsSyncFields } from "./AzureDevOpsSyncFields"; import { AzureKeyVaultSyncFields } from "./AzureKeyVaultSyncFields"; import { CamundaSyncFields } from "./CamundaSyncFields"; import { DatabricksSyncFields } from "./DatabricksSyncFields"; +import { FlyioSyncFields } from "./FlyioSyncFields"; import { GcpSyncFields } from "./GcpSyncFields"; import { GitHubSyncFields } from "./GitHubSyncFields"; import { HCVaultSyncFields } from "./HCVaultSyncFields"; @@ -61,6 +62,8 @@ export const SecretSyncDestinationFields = () => { return ; case SecretSync.OnePass: return ; + case SecretSync.Flyio: + return ; default: throw new Error(`Unhandled Destination Config Field: ${destination}`); } diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx index 0f6382edc..70c2fc498 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx @@ -52,6 +52,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => { case SecretSync.TeamCity: case SecretSync.OnePass: case SecretSync.OCIVault: + case SecretSync.Flyio: AdditionalSyncOptionsFieldsComponent = null; break; default: diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/FlyioSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/FlyioSyncReviewFields.tsx new file mode 100644 index 000000000..9a1a4bf69 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/FlyioSyncReviewFields.tsx @@ -0,0 +1,12 @@ +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 FlyioSyncReviewFields = () => { + const { watch } = useFormContext(); + const appId = watch("destinationConfig.appId"); + + return {appId}; +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx index 854555ffb..82d62d19b 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx @@ -20,6 +20,7 @@ import { AzureDevOpsSyncReviewFields } from "./AzureDevOpsSyncReviewFields"; import { AzureKeyVaultSyncReviewFields } from "./AzureKeyVaultSyncReviewFields"; import { CamundaSyncReviewFields } from "./CamundaSyncReviewFields"; import { DatabricksSyncReviewFields } from "./DatabricksSyncReviewFields"; +import { FlyioSyncReviewFields } from "./FlyioSyncReviewFields"; import { GcpSyncReviewFields } from "./GcpSyncReviewFields"; import { GitHubSyncReviewFields } from "./GitHubSyncReviewFields"; import { HCVaultSyncReviewFields } from "./HCVaultSyncReviewFields"; @@ -104,6 +105,9 @@ export const SecretSyncReviewFields = () => { case SecretSync.OnePass: DestinationFieldsComponent = ; break; + case SecretSync.Flyio: + DestinationFieldsComponent = ; + break; default: throw new Error(`Unhandled Destination Review Fields: ${destination}`); } diff --git a/frontend/src/components/secret-syncs/forms/schemas/flyio-sync-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/flyio-sync-destination-schema.ts new file mode 100644 index 000000000..b18081ccc --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/schemas/flyio-sync-destination-schema.ts @@ -0,0 +1,13 @@ +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 FlyioSyncDestinationSchema = BaseSecretSyncSchema().merge( + z.object({ + destination: z.literal(SecretSync.Flyio), + destinationConfig: z.object({ + appId: z.string().trim().min(1, "App ID required") + }) + }) +); 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 5dfb643b4..a10863f17 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 @@ -8,6 +8,7 @@ import { AzureDevOpsSyncDestinationSchema } from "./azure-devops-sync-destinatio import { AzureKeyVaultSyncDestinationSchema } from "./azure-key-vault-sync-destination-schema"; import { CamundaSyncDestinationSchema } from "./camunda-sync-destination-schema"; import { DatabricksSyncDestinationSchema } from "./databricks-sync-destination-schema"; +import { FlyioSyncDestinationSchema } from "./flyio-sync-destination-schema"; import { GcpSyncDestinationSchema } from "./gcp-sync-destination-schema"; import { GitHubSyncDestinationSchema } from "./github-sync-destination-schema"; import { HCVaultSyncDestinationSchema } from "./hc-vault-sync-destination-schema"; @@ -35,7 +36,8 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [ HCVaultSyncDestinationSchema, TeamCitySyncDestinationSchema, OCIVaultSyncDestinationSchema, - OnePassSyncDestinationSchema + OnePassSyncDestinationSchema, + FlyioSyncDestinationSchema ]); export const SecretSyncFormSchema = SecretSyncUnionSchema; diff --git a/frontend/src/helpers/appConnections.ts b/frontend/src/helpers/appConnections.ts index 7a87de58c..1efcd23ee 100644 --- a/frontend/src/helpers/appConnections.ts +++ b/frontend/src/helpers/appConnections.ts @@ -19,6 +19,7 @@ import { AzureKeyVaultConnectionMethod, CamundaConnectionMethod, DatabricksConnectionMethod, + FlyioConnectionMethod, GcpConnectionMethod, GitHubConnectionMethod, GitHubRadarConnectionMethod, @@ -78,7 +79,8 @@ export const APP_CONNECTION_MAP: Record< [AppConnection.LDAP]: { name: "LDAP", image: "LDAP.png", size: 65 }, [AppConnection.TeamCity]: { name: "TeamCity", image: "TeamCity.png" }, [AppConnection.OCI]: { name: "OCI", image: "Oracle.png", enterprise: true }, - [AppConnection.OnePass]: { name: "1Password", image: "1Password.png" } + [AppConnection.OnePass]: { name: "1Password", image: "1Password.png" }, + [AppConnection.Flyio]: { name: "Fly.io", image: "Flyio.svg" } }; export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => { @@ -117,6 +119,7 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) case TeamCityConnectionMethod.AccessToken: case AzureDevOpsConnectionMethod.AccessToken: case WindmillConnectionMethod.AccessToken: + case FlyioConnectionMethod.AccessToken: return { name: "Access Token", icon: faKey }; case Auth0ConnectionMethod.ClientCredentials: return { name: "Client Credentials", icon: faServer }; diff --git a/frontend/src/helpers/secretSyncs.ts b/frontend/src/helpers/secretSyncs.ts index 98a519e33..69d8804f8 100644 --- a/frontend/src/helpers/secretSyncs.ts +++ b/frontend/src/helpers/secretSyncs.ts @@ -60,6 +60,10 @@ export const SECRET_SYNC_MAP: Record = { [SecretSync.HCVault]: AppConnection.HCVault, [SecretSync.TeamCity]: AppConnection.TeamCity, [SecretSync.OCIVault]: AppConnection.OCI, - [SecretSync.OnePass]: AppConnection.OnePass + [SecretSync.OnePass]: AppConnection.OnePass, + [SecretSync.Flyio]: AppConnection.Flyio }; export const SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP: Record< diff --git a/frontend/src/hooks/api/appConnections/enums.ts b/frontend/src/hooks/api/appConnections/enums.ts index 1a3afe355..5073d16b7 100644 --- a/frontend/src/hooks/api/appConnections/enums.ts +++ b/frontend/src/hooks/api/appConnections/enums.ts @@ -22,5 +22,6 @@ export enum AppConnection { LDAP = "ldap", TeamCity = "teamcity", OCI = "oci", - OnePass = "1password" + OnePass = "1password", + Flyio = "flyio" } diff --git a/frontend/src/hooks/api/appConnections/flyio/index.ts b/frontend/src/hooks/api/appConnections/flyio/index.ts new file mode 100644 index 000000000..2c1906d36 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/flyio/index.ts @@ -0,0 +1,2 @@ +export * from "./queries"; +export * from "./types"; diff --git a/frontend/src/hooks/api/appConnections/flyio/queries.tsx b/frontend/src/hooks/api/appConnections/flyio/queries.tsx new file mode 100644 index 000000000..32c1ffbc2 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/flyio/queries.tsx @@ -0,0 +1,36 @@ +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { appConnectionKeys } from "../queries"; +import { TFlyioApp } from "./types"; + +const flyioConnectionKeys = { + all: [...appConnectionKeys.all, "flyio"] as const, + listApps: (connectionId: string) => [...flyioConnectionKeys.all, "apps", connectionId] as const +}; + +export const useFlyioConnectionListApps = ( + connectionId: string, + options?: Omit< + UseQueryOptions< + TFlyioApp[], + unknown, + TFlyioApp[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: flyioConnectionKeys.listApps(connectionId), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/app-connections/flyio/${connectionId}/apps` + ); + + return data; + }, + ...options + }); +}; diff --git a/frontend/src/hooks/api/appConnections/flyio/types.ts b/frontend/src/hooks/api/appConnections/flyio/types.ts new file mode 100644 index 000000000..73345e628 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/flyio/types.ts @@ -0,0 +1,4 @@ +export type TFlyioApp = { + id: string; + name: string; +}; diff --git a/frontend/src/hooks/api/appConnections/types/app-options.ts b/frontend/src/hooks/api/appConnections/types/app-options.ts index 0dcb11fa6..a1a916896 100644 --- a/frontend/src/hooks/api/appConnections/types/app-options.ts +++ b/frontend/src/hooks/api/appConnections/types/app-options.ts @@ -110,6 +110,10 @@ export type TOnePassConnectionOption = TAppConnectionOptionBase & { app: AppConnection.OnePass; }; +export type TFlyioConnectionOption = TAppConnectionOptionBase & { + app: AppConnection.Flyio; +}; + export type TAppConnectionOption = | TAwsConnectionOption | TGitHubConnectionOption @@ -132,7 +136,8 @@ export type TAppConnectionOption = | THCVaultConnectionOption | TTeamCityConnectionOption | TOCIConnectionOption - | TOnePassConnectionOption; + | TOnePassConnectionOption + | TFlyioConnectionOption; export type TAppConnectionOptionMap = { [AppConnection.AWS]: TAwsConnectionOption; @@ -159,4 +164,5 @@ export type TAppConnectionOptionMap = { [AppConnection.TeamCity]: TTeamCityConnectionOption; [AppConnection.OCI]: TOCIConnectionOption; [AppConnection.OnePass]: TOnePassConnectionOption; + [AppConnection.Flyio]: TFlyioConnectionOption; }; diff --git a/frontend/src/hooks/api/appConnections/types/flyio-connection.ts b/frontend/src/hooks/api/appConnections/types/flyio-connection.ts new file mode 100644 index 000000000..b1c9123b6 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/types/flyio-connection.ts @@ -0,0 +1,13 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection"; + +export enum FlyioConnectionMethod { + AccessToken = "access-token" +} + +export type TFlyioConnection = TRootAppConnection & { app: AppConnection.Flyio } & { + method: FlyioConnectionMethod.AccessToken; + credentials: { + accessToken: string; + }; +}; diff --git a/frontend/src/hooks/api/appConnections/types/index.ts b/frontend/src/hooks/api/appConnections/types/index.ts index 890562b6b..8a57efa35 100644 --- a/frontend/src/hooks/api/appConnections/types/index.ts +++ b/frontend/src/hooks/api/appConnections/types/index.ts @@ -9,6 +9,7 @@ import { TAzureDevOpsConnection } from "./azure-devops-connection"; import { TAzureKeyVaultConnection } from "./azure-key-vault-connection"; import { TCamundaConnection } from "./camunda-connection"; import { TDatabricksConnection } from "./databricks-connection"; +import { TFlyioConnection } from "./flyio-connection"; import { TGcpConnection } from "./gcp-connection"; import { TGitHubConnection } from "./github-connection"; import { TGitHubRadarConnection } from "./github-radar-connection"; @@ -34,6 +35,7 @@ export * from "./azure-devops-connection"; export * from "./azure-key-vault-connection"; export * from "./camunda-connection"; export * from "./databricks-connection"; +export * from "./flyio-connection"; export * from "./gcp-connection"; export * from "./github-connection"; export * from "./github-radar-connection"; @@ -74,7 +76,8 @@ export type TAppConnection = | TLdapConnection | TTeamCityConnection | TOCIConnection - | TOnePassConnection; + | TOnePassConnection + | TFlyioConnection; export type TAvailableAppConnection = Pick; @@ -126,4 +129,5 @@ export type TAppConnectionMap = { [AppConnection.TeamCity]: TTeamCityConnection; [AppConnection.OCI]: TOCIConnection; [AppConnection.OnePass]: TOnePassConnection; + [AppConnection.Flyio]: TFlyioConnection; }; diff --git a/frontend/src/hooks/api/secretSyncs/enums.ts b/frontend/src/hooks/api/secretSyncs/enums.ts index f381b3fca..17a5e0983 100644 --- a/frontend/src/hooks/api/secretSyncs/enums.ts +++ b/frontend/src/hooks/api/secretSyncs/enums.ts @@ -15,7 +15,8 @@ export enum SecretSync { HCVault = "hashicorp-vault", TeamCity = "teamcity", OCIVault = "oci-vault", - OnePass = "1password" + OnePass = "1password", + Flyio = "flyio" } export enum SecretSyncStatus { diff --git a/frontend/src/hooks/api/secretSyncs/types/flyio-sync.ts b/frontend/src/hooks/api/secretSyncs/types/flyio-sync.ts new file mode 100644 index 000000000..0717de2c4 --- /dev/null +++ b/frontend/src/hooks/api/secretSyncs/types/flyio-sync.ts @@ -0,0 +1,15 @@ +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 TFlyioSync = TRootSecretSync & { + destination: SecretSync.Flyio; + destinationConfig: { + appId: string; + }; + connection: { + app: AppConnection.Flyio; + 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 4f447f9df..0d5bbdb56 100644 --- a/frontend/src/hooks/api/secretSyncs/types/index.ts +++ b/frontend/src/hooks/api/secretSyncs/types/index.ts @@ -9,6 +9,7 @@ import { TAzureDevOpsSync } from "./azure-devops-sync"; import { TAzureKeyVaultSync } from "./azure-key-vault-sync"; import { TCamundaSync } from "./camunda-sync"; import { TDatabricksSync } from "./databricks-sync"; +import { TFlyioSync } from "./flyio-sync"; import { TGcpSync } from "./gcp-sync"; import { TGitHubSync } from "./github-sync"; import { THCVaultSync } from "./hc-vault-sync"; @@ -43,7 +44,8 @@ export type TSecretSync = | THCVaultSync | TTeamCitySync | TOCIVaultSync - | TOnePassSync; + | TOnePassSync + | TFlyioSync; export type TListSecretSyncs = { secretSyncs: TSecretSync[] }; diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx index 052ff18a7..3882df698 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx @@ -18,6 +18,7 @@ import { AzureDevOpsConnectionForm } from "./AzureDevOpsConnectionForm"; import { AzureKeyVaultConnectionForm } from "./AzureKeyVaultConnectionForm"; import { CamundaConnectionForm } from "./CamundaConnectionForm"; import { DatabricksConnectionForm } from "./DatabricksConnectionForm"; +import { FlyioConnectionForm } from "./FlyioConnectionForm"; import { GcpConnectionForm } from "./GcpConnectionForm"; import { GitHubConnectionForm } from "./GitHubConnectionForm"; import { GitHubRadarConnectionForm } from "./GitHubRadarConnectionForm"; @@ -119,6 +120,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => { return ; case AppConnection.OnePass: return ; + case AppConnection.Flyio: + return ; default: throw new Error(`Unhandled App ${app}`); } @@ -203,6 +206,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => { return ; case AppConnection.OnePass: return ; + case AppConnection.Flyio: + return ; default: throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`); } diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/FlyioConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/FlyioConnectionForm.tsx new file mode 100644 index 000000000..4b6c5bbc9 --- /dev/null +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/FlyioConnectionForm.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 { FlyioConnectionMethod, TFlyioConnection } from "@app/hooks/api/appConnections"; +import { AppConnection } from "@app/hooks/api/appConnections/enums"; + +import { + genericAppConnectionFieldsSchema, + GenericAppConnectionsFields +} from "./GenericAppConnectionFields"; + +type Props = { + appConnection?: TFlyioConnection; + onSubmit: (formData: FormData) => void; +}; + +const rootSchema = genericAppConnectionFieldsSchema.extend({ + app: z.literal(AppConnection.Flyio) +}); + +const formSchema = z.discriminatedUnion("method", [ + rootSchema.extend({ + method: z.literal(FlyioConnectionMethod.AccessToken), + credentials: z.object({ + accessToken: z + .string() + .trim() + .min(1, "Access Token required") + .startsWith("FlyV1", "Token must start with 'FlyV1'") + }) + }) +]); + +type FormData = z.infer; + +export const FlyioConnectionForm = ({ appConnection, onSubmit }: Props) => { + const isUpdate = Boolean(appConnection); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: appConnection ?? { + app: AppConnection.Flyio, + method: FlyioConnectionMethod.AccessToken + } + }); + + 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/FlyioSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/FlyioSyncDestinationCol.tsx new file mode 100644 index 000000000..4b69e8348 --- /dev/null +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/FlyioSyncDestinationCol.tsx @@ -0,0 +1,14 @@ +import { TFlyioSync } from "@app/hooks/api/secretSyncs/types/flyio-sync"; + +import { getSecretSyncDestinationColValues } from "../helpers"; +import { SecretSyncTableCell } from "../SecretSyncTableCell"; + +type Props = { + secretSync: TFlyioSync; +}; + +export const FlyioSyncDestinationCol = ({ 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 f342c361c..87c03aacb 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 @@ -8,6 +8,7 @@ import { AzureDevOpsSyncDestinationCol } from "./AzureDevOpsSyncDestinationCol"; import { AzureKeyVaultDestinationSyncCol } from "./AzureKeyVaultDestinationSyncCol"; import { CamundaSyncDestinationCol } from "./CamundaSyncDestinationCol"; import { DatabricksSyncDestinationCol } from "./DatabricksSyncDestinationCol"; +import { FlyioSyncDestinationCol } from "./FlyioSyncDestinationCol"; import { GcpSyncDestinationCol } from "./GcpSyncDestinationCol"; import { GitHubSyncDestinationCol } from "./GitHubSyncDestinationCol"; import { HCVaultSyncDestinationCol } from "./HCVaultSyncDestinationCol"; @@ -58,6 +59,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => { return ; case SecretSync.AzureDevOps: return ; + case SecretSync.Flyio: + return ; default: throw new Error( `Unhandled Secret Sync Destination Col: ${(secretSync as TSecretSync).destination}` 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 3c1056382..06e9a5a48 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts @@ -116,6 +116,10 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => { primaryText = destinationConfig.devopsProjectName; secondaryText = destinationConfig.devopsProjectId; break; + case SecretSync.Flyio: + primaryText = destinationConfig.appId; + secondaryText = "App ID"; + break; default: throw new Error(`Unhandled Destination Col Values ${destination}`); } diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/FlyioSyncDestinationSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/FlyioSyncDestinationSection.tsx new file mode 100644 index 000000000..f5132d04b --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/FlyioSyncDestinationSection.tsx @@ -0,0 +1,14 @@ +import { GenericFieldLabel } from "@app/components/secret-syncs"; +import { TFlyioSync } from "@app/hooks/api/secretSyncs/types/flyio-sync"; + +type Props = { + secretSync: TFlyioSync; +}; + +export const FlyioSyncDestinationSection = ({ secretSync }: Props) => { + const { + destinationConfig: { appId } + } = secretSync; + + return {appId}; +}; 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 24cc3908a..5426c9ce5 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx @@ -18,6 +18,7 @@ import { AzureDevOpsSyncDestinationSection } from "./AzureDevOpsSyncDestinationS import { AzureKeyVaultSyncDestinationSection } from "./AzureKeyVaultSyncDestinationSection"; import { CamundaSyncDestinationSection } from "./CamundaSyncDestinationSection"; import { DatabricksSyncDestinationSection } from "./DatabricksSyncDestinationSection"; +import { FlyioSyncDestinationSection } from "./FlyioSyncDestinationSection"; import { GcpSyncDestinationSection } from "./GcpSyncDestinationSection"; import { GitHubSyncDestinationSection } from "./GitHubSyncDestinationSection"; import { HCVaultSyncDestinationSection } from "./HCVaultSyncDestinationSection"; @@ -93,6 +94,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }: case SecretSync.AzureDevOps: DestinationComponents = ; break; + case SecretSync.Flyio: + DestinationComponents = ; + break; default: throw new Error(`Unhandled Destination Section components: ${destination}`); } 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 e02d2ff84..85d9d9a64 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx @@ -52,6 +52,7 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) = case SecretSync.TeamCity: case SecretSync.OCIVault: case SecretSync.OnePass: + case SecretSync.Flyio: AdditionalSyncOptionsComponent = null; break; default: