mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-16 12:04:03 +00:00
feat: Digital Ocean App connection and App Platform secret sync (#4203)
* fix: save wip * feat: final impl * feat: docs * Update backend/src/services/app-connection/digital-ocean/digital-ocean-connection-service.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * chore: remove empty conflict files * Update backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Update frontend/src/components/secret-syncs/forms/schemas/digital-ocean-app-platform-sync-destination-schema.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Update frontend/src/components/secret-syncs/forms/schemas/digital-ocean-app-platform-sync-destination-schema.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Update frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/DigitalOceanAppPlatformSyncFields.tsx Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Update backend/src/services/secret-sync/digital-ocean-app-platform/digital-ocean-app-platform-sync-schemas.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * fix: lint * fix: api client * fix: lint and types * fix: typecheck lint * fix: docs * fix: docs * fix: linting --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
greptile-apps[bot]
parent
95b997c100
commit
ab56a69d59
@@ -33,6 +33,7 @@ export enum AppConnection {
|
||||
Bitbucket = "bitbucket",
|
||||
Checkly = "checkly",
|
||||
Supabase = "supabase",
|
||||
DigitalOcean = "digital-ocean",
|
||||
Okta = "okta"
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +68,11 @@ import {
|
||||
getDatabricksConnectionListItem,
|
||||
validateDatabricksConnectionCredentials
|
||||
} from "./databricks";
|
||||
import {
|
||||
DigitalOceanConnectionMethod,
|
||||
getDigitalOceanConnectionListItem,
|
||||
validateDigitalOceanConnectionCredentials
|
||||
} from "./digital-ocean";
|
||||
import { FlyioConnectionMethod, getFlyioConnectionListItem, validateFlyioConnectionCredentials } from "./flyio";
|
||||
import { GcpConnectionMethod, getGcpConnectionListItem, validateGcpConnectionCredentials } from "./gcp";
|
||||
import { getGitHubConnectionListItem, GitHubConnectionMethod, validateGitHubConnectionCredentials } from "./github";
|
||||
@@ -157,6 +162,7 @@ export const listAppConnectionOptions = () => {
|
||||
getBitbucketConnectionListItem(),
|
||||
getChecklyConnectionListItem(),
|
||||
getSupabaseConnectionListItem(),
|
||||
getDigitalOceanConnectionListItem(),
|
||||
getOktaConnectionListItem()
|
||||
].sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
@@ -244,6 +250,7 @@ export const validateAppConnectionCredentials = async (
|
||||
[AppConnection.Bitbucket]: validateBitbucketConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Checkly]: validateChecklyConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Supabase]: validateSupabaseConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.DigitalOcean]: validateDigitalOceanConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Okta]: validateOktaConnectionCredentials as TAppConnectionCredentialsValidator
|
||||
};
|
||||
|
||||
@@ -283,6 +290,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
||||
case CloudflareConnectionMethod.APIToken:
|
||||
case BitbucketConnectionMethod.ApiToken:
|
||||
case ZabbixConnectionMethod.ApiToken:
|
||||
case DigitalOceanConnectionMethod.ApiToken:
|
||||
case OktaConnectionMethod.ApiToken:
|
||||
return "API Token";
|
||||
case PostgresConnectionMethod.UsernameAndPassword:
|
||||
@@ -372,6 +380,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
|
||||
[AppConnection.Bitbucket]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Checkly]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Supabase]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.DigitalOcean]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Okta]: platformManagedCredentialsNotSupported
|
||||
};
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
||||
[AppConnection.Bitbucket]: "Bitbucket",
|
||||
[AppConnection.Checkly]: "Checkly",
|
||||
[AppConnection.Supabase]: "Supabase",
|
||||
[AppConnection.DigitalOcean]: "DigitalOcean App Platform",
|
||||
[AppConnection.Okta]: "Okta"
|
||||
};
|
||||
|
||||
@@ -73,5 +74,6 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
|
||||
[AppConnection.Bitbucket]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Checkly]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Supabase]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.DigitalOcean]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Okta]: AppConnectionPlanType.Regular
|
||||
};
|
||||
|
||||
@@ -61,6 +61,8 @@ import { ValidateCloudflareConnectionCredentialsSchema } from "./cloudflare/clou
|
||||
import { cloudflareConnectionService } from "./cloudflare/cloudflare-connection-service";
|
||||
import { ValidateDatabricksConnectionCredentialsSchema } from "./databricks";
|
||||
import { databricksConnectionService } from "./databricks/databricks-connection-service";
|
||||
import { ValidateDigitalOceanConnectionCredentialsSchema } from "./digital-ocean";
|
||||
import { digitalOceanAppPlatformConnectionService } from "./digital-ocean/digital-ocean-connection-service";
|
||||
import { ValidateFlyioConnectionCredentialsSchema } from "./flyio";
|
||||
import { flyioConnectionService } from "./flyio/flyio-connection-service";
|
||||
import { ValidateGcpConnectionCredentialsSchema } from "./gcp";
|
||||
@@ -145,6 +147,7 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
|
||||
[AppConnection.Bitbucket]: ValidateBitbucketConnectionCredentialsSchema,
|
||||
[AppConnection.Checkly]: ValidateChecklyConnectionCredentialsSchema,
|
||||
[AppConnection.Supabase]: ValidateSupabaseConnectionCredentialsSchema,
|
||||
[AppConnection.DigitalOcean]: ValidateDigitalOceanConnectionCredentialsSchema,
|
||||
[AppConnection.Okta]: ValidateOktaConnectionCredentialsSchema
|
||||
};
|
||||
|
||||
@@ -607,6 +610,7 @@ export const appConnectionServiceFactory = ({
|
||||
bitbucket: bitbucketConnectionService(connectAppConnectionById),
|
||||
checkly: checklyConnectionService(connectAppConnectionById),
|
||||
supabase: supabaseConnectionService(connectAppConnectionById),
|
||||
digitalOcean: digitalOceanAppPlatformConnectionService(connectAppConnectionById),
|
||||
okta: oktaConnectionService(connectAppConnectionById)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -87,6 +87,12 @@ import {
|
||||
TDatabricksConnectionInput,
|
||||
TValidateDatabricksConnectionCredentialsSchema
|
||||
} from "./databricks";
|
||||
import {
|
||||
TDigitalOceanConnection,
|
||||
TDigitalOceanConnectionConfig,
|
||||
TDigitalOceanConnectionInput,
|
||||
TValidateDigitalOceanCredentialsSchema
|
||||
} from "./digital-ocean";
|
||||
import {
|
||||
TFlyioConnection,
|
||||
TFlyioConnectionConfig,
|
||||
@@ -238,6 +244,7 @@ export type TAppConnection = { id: string } & (
|
||||
| TRailwayConnection
|
||||
| TChecklyConnection
|
||||
| TSupabaseConnection
|
||||
| TDigitalOceanConnection
|
||||
| TOktaConnection
|
||||
);
|
||||
|
||||
@@ -280,6 +287,7 @@ export type TAppConnectionInput = { id: string } & (
|
||||
| TRailwayConnectionInput
|
||||
| TChecklyConnectionInput
|
||||
| TSupabaseConnectionInput
|
||||
| TDigitalOceanConnectionInput
|
||||
| TOktaConnectionInput
|
||||
);
|
||||
|
||||
@@ -330,6 +338,7 @@ export type TAppConnectionConfig =
|
||||
| TRailwayConnectionConfig
|
||||
| TChecklyConnectionConfig
|
||||
| TSupabaseConnectionConfig
|
||||
| TDigitalOceanConnectionConfig
|
||||
| TOktaConnectionConfig;
|
||||
|
||||
export type TValidateAppConnectionCredentialsSchema =
|
||||
@@ -367,6 +376,7 @@ export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateRailwayConnectionCredentialsSchema
|
||||
| TValidateChecklyConnectionCredentialsSchema
|
||||
| TValidateSupabaseConnectionCredentialsSchema
|
||||
| TValidateDigitalOceanCredentialsSchema
|
||||
| TValidateOktaConnectionCredentialsSchema;
|
||||
|
||||
export type TListAwsConnectionKmsKeys = {
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export enum DigitalOceanConnectionMethod {
|
||||
ApiToken = "api-token"
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import { AxiosError } from "axios";
|
||||
import { z } from "zod";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
import { DigitalOceanConnectionMethod } from "./digital-ocean-connection-constants";
|
||||
import { DigitalOceanAppPlatformPublicAPI } from "./digital-ocean-connection-public-client";
|
||||
import { DigitalOceanConnectionListItemSchema } from "./digital-ocean-connection-schemas";
|
||||
import { TDigitalOceanConnectionConfig } from "./digital-ocean-connection-types";
|
||||
|
||||
export const getDigitalOceanConnectionListItem = () => {
|
||||
return {
|
||||
name: "Digital Ocean" as z.infer<typeof DigitalOceanConnectionListItemSchema>["name"],
|
||||
app: AppConnection.DigitalOcean as const,
|
||||
methods: Object.values(DigitalOceanConnectionMethod)
|
||||
};
|
||||
};
|
||||
|
||||
export const validateDigitalOceanConnectionCredentials = async (config: TDigitalOceanConnectionConfig) => {
|
||||
try {
|
||||
await DigitalOceanAppPlatformPublicAPI.healthcheck(config);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to validate credentials: ${error.message || "Unknown error"}`
|
||||
});
|
||||
}
|
||||
|
||||
throw new BadRequestError({
|
||||
message: "Unable to validate connection - verify credentials"
|
||||
});
|
||||
}
|
||||
|
||||
return config.credentials;
|
||||
};
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
/* eslint-disable class-methods-use-this */
|
||||
import { AxiosInstance } from "axios";
|
||||
|
||||
import { createRequestClient } from "@app/lib/config/request";
|
||||
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||
|
||||
import { DigitalOceanConnectionMethod } from "./digital-ocean-connection-constants";
|
||||
import {
|
||||
TDigitalOceanApp,
|
||||
TDigitalOceanConnectionConfig,
|
||||
TDigitalOceanVariable
|
||||
} from "./digital-ocean-connection-types";
|
||||
|
||||
class DigitalOceanAppPlatformPublicClient {
|
||||
private readonly client: AxiosInstance;
|
||||
|
||||
constructor() {
|
||||
this.client = createRequestClient({
|
||||
baseURL: `${IntegrationUrls.DIGITAL_OCEAN_API_URL}/v2`,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async healthcheck(connection: TDigitalOceanConnectionConfig) {
|
||||
switch (connection.method) {
|
||||
case DigitalOceanConnectionMethod.ApiToken:
|
||||
await this.getApps(connection);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported connection method`);
|
||||
}
|
||||
}
|
||||
|
||||
async getApps(connection: TDigitalOceanConnectionConfig) {
|
||||
const response = await this.client.get<{ apps: TDigitalOceanApp[] }>(`/apps`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${connection.credentials.apiToken}`
|
||||
}
|
||||
});
|
||||
|
||||
return response.data.apps;
|
||||
}
|
||||
|
||||
async getApp(connection: TDigitalOceanConnectionConfig, appId: string) {
|
||||
const response = await this.client.get<{ app: TDigitalOceanApp }>(`/apps/${appId}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${connection.credentials.apiToken}`
|
||||
}
|
||||
});
|
||||
|
||||
return response.data.app;
|
||||
}
|
||||
|
||||
async getVariables(connection: TDigitalOceanConnectionConfig, appId: string): Promise<TDigitalOceanVariable[]> {
|
||||
const app = await this.getApp(connection, appId);
|
||||
return app.spec.envs || [];
|
||||
}
|
||||
|
||||
async putVariables(connection: TDigitalOceanConnectionConfig, appId: string, ...input: TDigitalOceanVariable[]) {
|
||||
const response = await this.getApp(connection, appId);
|
||||
|
||||
return this.client.put(
|
||||
`/apps/${appId}`,
|
||||
{
|
||||
spec: {
|
||||
...response.spec,
|
||||
envs: input
|
||||
}
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${connection.credentials.apiToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async deleteVariables(connection: TDigitalOceanConnectionConfig, appId: string, ...input: TDigitalOceanVariable[]) {
|
||||
const response = await this.getApp(connection, appId);
|
||||
const existing = response.spec.envs || [];
|
||||
|
||||
const variables = existing.filter((v) => input.find((i) => i.key === v.key));
|
||||
|
||||
return this.client.put(
|
||||
`/apps/${appId}`,
|
||||
{
|
||||
spec: {
|
||||
...response.spec,
|
||||
envs: variables
|
||||
}
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${connection.credentials.apiToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const DigitalOceanAppPlatformPublicAPI = new DigitalOceanAppPlatformPublicClient();
|
||||
@@ -0,0 +1,67 @@
|
||||
import z from "zod";
|
||||
|
||||
import { AppConnections } from "@app/lib/api-docs";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
BaseAppConnectionSchema,
|
||||
GenericCreateAppConnectionFieldsSchema,
|
||||
GenericUpdateAppConnectionFieldsSchema
|
||||
} from "@app/services/app-connection/app-connection-schemas";
|
||||
|
||||
import { DigitalOceanConnectionMethod } from "./digital-ocean-connection-constants";
|
||||
|
||||
export const DigitalOceanConnectionMethodSchema = z
|
||||
.nativeEnum(DigitalOceanConnectionMethod)
|
||||
.describe(AppConnections.CREATE(AppConnection.DigitalOcean).method);
|
||||
|
||||
export const DigitalOceanConnectionAccessTokenCredentialsSchema = z.object({
|
||||
apiToken: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "API Token required")
|
||||
.max(255)
|
||||
.describe(AppConnections.CREDENTIALS.DIGITAL_OCEAN_APP_PLATFORM.apiToken)
|
||||
});
|
||||
|
||||
const BaseDigitalOceanConnectionSchema = BaseAppConnectionSchema.extend({
|
||||
app: z.literal(AppConnection.DigitalOcean)
|
||||
});
|
||||
|
||||
export const DigitalOceanConnectionSchema = BaseDigitalOceanConnectionSchema.extend({
|
||||
method: DigitalOceanConnectionMethodSchema,
|
||||
credentials: DigitalOceanConnectionAccessTokenCredentialsSchema
|
||||
});
|
||||
|
||||
export const SanitizedDigitalOceanConnectionSchema = z.discriminatedUnion("method", [
|
||||
BaseDigitalOceanConnectionSchema.extend({
|
||||
method: DigitalOceanConnectionMethodSchema,
|
||||
credentials: DigitalOceanConnectionAccessTokenCredentialsSchema.pick({})
|
||||
})
|
||||
]);
|
||||
|
||||
export const ValidateDigitalOceanConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: DigitalOceanConnectionMethodSchema,
|
||||
credentials: DigitalOceanConnectionAccessTokenCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.DigitalOcean).credentials
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
export const CreateDigitalOceanConnectionSchema = ValidateDigitalOceanConnectionCredentialsSchema.and(
|
||||
GenericCreateAppConnectionFieldsSchema(AppConnection.DigitalOcean)
|
||||
);
|
||||
|
||||
export const UpdateDigitalOceanConnectionSchema = z
|
||||
.object({
|
||||
credentials: DigitalOceanConnectionAccessTokenCredentialsSchema.optional().describe(
|
||||
AppConnections.UPDATE(AppConnection.DigitalOcean).credentials
|
||||
)
|
||||
})
|
||||
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.DigitalOcean));
|
||||
|
||||
export const DigitalOceanConnectionListItemSchema = z.object({
|
||||
name: z.literal("Digital Ocean"),
|
||||
app: z.literal(AppConnection.DigitalOcean),
|
||||
methods: z.nativeEnum(DigitalOceanConnectionMethod).array()
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { DigitalOceanAppPlatformPublicAPI } from "./digital-ocean-connection-public-client";
|
||||
import { TDigitalOceanConnection } from "./digital-ocean-connection-types";
|
||||
|
||||
type TGetAppConnectionFunc = (
|
||||
app: AppConnection,
|
||||
connectionId: string,
|
||||
actor: OrgServiceActor
|
||||
) => Promise<TDigitalOceanConnection>;
|
||||
|
||||
export const digitalOceanAppPlatformConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
|
||||
const listApps = async (connectionId: string, actor: OrgServiceActor) => {
|
||||
const connection = await getAppConnection(AppConnection.DigitalOcean, connectionId, actor);
|
||||
try {
|
||||
const apps = await DigitalOceanAppPlatformPublicAPI.getApps(connection);
|
||||
return apps;
|
||||
} catch (error) {
|
||||
logger.error(error, "Failed to list apps on Digital Ocean");
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
listApps
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
import z from "zod";
|
||||
|
||||
import { DiscriminativePick } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import {
|
||||
CreateDigitalOceanConnectionSchema,
|
||||
DigitalOceanConnectionSchema,
|
||||
ValidateDigitalOceanConnectionCredentialsSchema
|
||||
} from "./digital-ocean-connection-schemas";
|
||||
|
||||
export type TDigitalOceanConnection = z.infer<typeof DigitalOceanConnectionSchema>;
|
||||
|
||||
export type TDigitalOceanConnectionInput = z.infer<typeof CreateDigitalOceanConnectionSchema> & {
|
||||
app: AppConnection.DigitalOcean;
|
||||
};
|
||||
|
||||
export type TValidateDigitalOceanCredentialsSchema = typeof ValidateDigitalOceanConnectionCredentialsSchema;
|
||||
|
||||
export type TDigitalOceanConnectionConfig = DiscriminativePick<
|
||||
TDigitalOceanConnection,
|
||||
"method" | "app" | "credentials"
|
||||
> & {
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type TDigitalOceanVariable = {
|
||||
key: string;
|
||||
value: string;
|
||||
type: "SECRET" | "GENERAL";
|
||||
};
|
||||
|
||||
export type TDigitalOceanApp = {
|
||||
id: string;
|
||||
spec: {
|
||||
name: string;
|
||||
services: Array<{
|
||||
name: string;
|
||||
}>;
|
||||
envs?: TDigitalOceanVariable[];
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./digital-ocean-connection-constants";
|
||||
export * from "./digital-ocean-connection-fns";
|
||||
export * from "./digital-ocean-connection-schemas";
|
||||
export * from "./digital-ocean-connection-types";
|
||||
Reference in New Issue
Block a user