Fly.io secret sync

This commit is contained in:
x032205
2025-06-16 15:49:42 -04:00
parent da5f054a65
commit fe75627ab7
33 changed files with 425 additions and 19 deletions
+3
View File
@@ -2387,6 +2387,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."
}
}
};
@@ -35,7 +35,6 @@ export const registerFlyioConnectionRouter = async (server: FastifyZodProvider)
response: {
200: z
.object({
// TODO(andrey): apps
id: z.string(),
name: z.string()
})
@@ -0,0 +1,13 @@
import { CreateFlyioSyncSchema, FlyioSyncSchema, UpdateFlyioSyncSchema } from "@app/services/secret-sync/flyio";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
export const registerFlyioSyncRouter = async (server: FastifyZodProvider) =>
registerSyncSecretsEndpoints({
destination: SecretSync.Flyio,
server,
responseSchema: FlyioSyncSchema,
createSchema: CreateFlyioSyncSchema,
updateSchema: UpdateFlyioSyncSchema
});
@@ -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<SecretSync, (server: Fastif
[SecretSync.HCVault]: registerHCVaultSyncRouter,
[SecretSync.TeamCity]: registerTeamCitySyncRouter,
[SecretSync.OCIVault]: registerOCIVaultSyncRouter,
[SecretSync.OnePass]: registerOnePassSyncRouter
[SecretSync.OnePass]: registerOnePassSyncRouter,
[SecretSync.Flyio]: registerFlyioSyncRouter
};
@@ -23,6 +23,7 @@ import { AzureDevOpsSyncListItemSchema, AzureDevOpsSyncSchema } from "@app/servi
import { AzureKeyVaultSyncListItemSchema, AzureKeyVaultSyncSchema } from "@app/services/secret-sync/azure-key-vault";
import { CamundaSyncListItemSchema, CamundaSyncSchema } from "@app/services/secret-sync/camunda";
import { DatabricksSyncListItemSchema, DatabricksSyncSchema } from "@app/services/secret-sync/databricks";
import { FlyioSyncListItemSchema, FlyioSyncSchema } from "@app/services/secret-sync/flyio";
import { GcpSyncListItemSchema, GcpSyncSchema } from "@app/services/secret-sync/gcp";
import { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret-sync/github";
import { HCVaultSyncListItemSchema, HCVaultSyncSchema } from "@app/services/secret-sync/hc-vault";
@@ -49,7 +50,8 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
HCVaultSyncSchema,
TeamCitySyncSchema,
OCIVaultSyncSchema,
OnePassSyncSchema
OnePassSyncSchema,
FlyioSyncSchema
]);
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
@@ -69,7 +71,8 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
HCVaultSyncListItemSchema,
TeamCitySyncListItemSchema,
OCIVaultSyncListItemSchema,
OnePassSyncListItemSchema
OnePassSyncListItemSchema,
FlyioSyncListItemSchema
]);
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {
@@ -21,7 +21,6 @@ export type TFlyioConnectionConfig = DiscriminativePick<TFlyioConnectionInput, "
orgId: string;
};
// TODO(andrey): apps
export type TFlyioApp = {
id: string;
name: string;
@@ -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
};
@@ -0,0 +1,132 @@
import { request } from "@app/lib/config/request";
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[] } } }>(
"https://api.fly.io/graphql",
{
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(
"https://api.fly.io/graphql",
{
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(
"https://api.fly.io/graphql",
{
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.`);
}
};
@@ -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").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)
});
@@ -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<typeof FlyioSyncSchema>;
export type TFlyioSyncInput = z.infer<typeof CreateFlyioSyncSchema>;
export type TFlyioSyncListItem = z.infer<typeof FlyioSyncListItemSchema>;
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[];
};
@@ -0,0 +1,4 @@
export * from "./flyio-sync-constants";
export * from "./flyio-sync-fns";
export * from "./flyio-sync-schemas";
export * from "./flyio-sync-types";
@@ -15,7 +15,8 @@ export enum SecretSync {
HCVault = "hashicorp-vault",
TeamCity = "teamcity",
OCIVault = "oci-vault",
OnePass = "1password"
OnePass = "1password",
Flyio = "flyio"
}
export enum SecretSyncInitialSyncBehavior {
@@ -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, TSecretSyncListItem> = {
[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}`
@@ -18,7 +18,8 @@ export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
[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<SecretSync, AppConnection> = {
@@ -38,7 +39,8 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
[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<SecretSync, SecretSyncPlanType> = {
@@ -58,5 +60,6 @@ export const SECRET_SYNC_PLAN_MAP: Record<SecretSync, SecretSyncPlanType> = {
[SecretSync.HCVault]: SecretSyncPlanType.Regular,
[SecretSync.TeamCity]: SecretSyncPlanType.Regular,
[SecretSync.OCIVault]: SecretSyncPlanType.Enterprise,
[SecretSync.OnePass]: SecretSyncPlanType.Regular
[SecretSync.OnePass]: SecretSyncPlanType.Regular,
[SecretSync.Flyio]: SecretSyncPlanType.Regular
};
@@ -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;