Merge pull request #5019 from Infisical/feat/octopus-deploy-secret-sync

feature: octopus deploy app-connection + secret-sync [ENG-4267]
This commit is contained in:
Scott Wilson
2025-12-17 15:21:02 -08:00
committed by GitHub
101 changed files with 2610 additions and 33 deletions

View File

@@ -2550,6 +2550,10 @@ export const AppConnections = {
orgName: "The short name of the Chef organization to connect to.",
userName: "The username used to access Chef.",
privateKey: "The private key used to access Chef."
},
OCTOPUS_DEPLOY: {
instanceUrl: "The Octopus Deploy instance URL to connect to.",
apiKey: "The API key used to authenticate with Octopus Deploy."
}
}
};
@@ -2710,6 +2714,14 @@ export const SecretSyncs = {
siteId: "The ID of the Laravel Forge site to sync secrets to.",
siteName: "The name of the Laravel Forge site to sync secrets to."
},
OCTOPUS_DEPLOY: {
spaceId: "The ID of the Octopus Deploy space to sync secrets to.",
spaceName: "The name of the Octopus Deploy space to sync secrets to.",
projectId: "The ID of the Octopus Deploy project to sync secrets to.",
projectName: "The name of the Octopus Deploy project to sync secrets to.",
scope: "The Octopus Deploy scope that secrets should be synced to.",
scopeValues: "The Octopus Deploy scope values that secrets should be synced to."
},
WINDMILL: {
workspace: "The Windmill workspace to sync secrets to.",
path: "The Windmill workspace path to sync secrets to."

View File

@@ -101,6 +101,10 @@ import {
NorthflankConnectionListItemSchema,
SanitizedNorthflankConnectionSchema
} from "@app/services/app-connection/northflank";
import {
OctopusDeployConnectionListItemSchema,
SanitizedOctopusDeployConnectionSchema
} from "@app/services/app-connection/octopus-deploy";
import { OktaConnectionListItemSchema, SanitizedOktaConnectionSchema } from "@app/services/app-connection/okta";
import {
PostgresConnectionListItemSchema,
@@ -180,7 +184,8 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedMongoDBConnectionSchema.options,
...SanitizedLaravelForgeConnectionSchema.options,
...SanitizedChefConnectionSchema.options,
...SanitizedDNSMadeEasyConnectionSchema.options
...SanitizedDNSMadeEasyConnectionSchema.options,
...SanitizedOctopusDeployConnectionSchema.options
]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
@@ -227,7 +232,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
MongoDBConnectionListItemSchema,
LaravelForgeConnectionListItemSchema,
ChefConnectionListItemSchema,
DNSMadeEasyConnectionListItemSchema
DNSMadeEasyConnectionListItemSchema,
OctopusDeployConnectionListItemSchema
]);
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {

View File

@@ -33,6 +33,7 @@ import { registerMsSqlConnectionRouter } from "./mssql-connection-router";
import { registerMySqlConnectionRouter } from "./mysql-connection-router";
import { registerNetlifyConnectionRouter } from "./netlify-connection-router";
import { registerNorthflankConnectionRouter } from "./northflank-connection-router";
import { registerOctopusDeployConnectionRouter } from "./octopus-deploy-connection-router";
import { registerOktaConnectionRouter } from "./okta-connection-router";
import { registerPostgresConnectionRouter } from "./postgres-connection-router";
import { registerRailwayConnectionRouter } from "./railway-connection-router";
@@ -92,5 +93,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
[AppConnection.Okta]: registerOktaConnectionRouter,
[AppConnection.Redis]: registerRedisConnectionRouter,
[AppConnection.MongoDB]: registerMongoDBConnectionRouter,
[AppConnection.Chef]: registerChefConnectionRouter
[AppConnection.Chef]: registerChefConnectionRouter,
[AppConnection.OctopusDeploy]: registerOctopusDeployConnectionRouter
};

View File

@@ -0,0 +1,168 @@
import { z } from "zod";
import { BadRequestError } from "@app/lib/errors";
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 {
CreateOctopusDeployConnectionSchema,
SanitizedOctopusDeployConnectionSchema,
UpdateOctopusDeployConnectionSchema
} from "@app/services/app-connection/octopus-deploy";
import { AuthMode } from "@app/services/auth/auth-type";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerOctopusDeployConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.OctopusDeploy,
server,
sanitizedResponseSchema: SanitizedOctopusDeployConnectionSchema,
createSchema: CreateOctopusDeployConnectionSchema,
updateSchema: UpdateOctopusDeployConnectionSchema
});
server.route({
method: "GET",
url: `/:connectionId/spaces`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
response: {
200: z.array(
z.object({
id: z.string(),
name: z.string(),
slug: z.string(),
isDefault: z.boolean()
})
)
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId } = req.params;
const spaces = await server.services.appConnection.octopusDeploy.listSpaces(connectionId, req.permission);
return spaces;
}
});
server.route({
method: "GET",
url: `/:connectionId/projects`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
querystring: z.object({
spaceId: z.string().min(1, "Space ID is required")
}),
response: {
200: z.array(
z.object({
id: z.string(),
name: z.string(),
slug: z.string()
})
)
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId } = req.params;
const { spaceId } = req.query;
const projects = await server.services.appConnection.octopusDeploy.listProjects(
connectionId,
spaceId,
req.permission
);
return projects;
}
});
server.route({
method: "GET",
url: `/:connectionId/scope-values`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
querystring: z.object({
spaceId: z.string().min(1, "Space ID is required"),
projectId: z.string().min(1, "Project ID is required")
}),
response: {
200: z.object({
environments: z
.object({
id: z.string(),
name: z.string()
})
.array(),
roles: z
.object({
id: z.string(),
name: z.string()
})
.array(),
machines: z
.object({
id: z.string(),
name: z.string()
})
.array(),
processes: z
.object({
id: z.string(),
name: z.string()
})
.array(),
actions: z
.object({
id: z.string(),
name: z.string()
})
.array(),
channels: z
.object({
id: z.string(),
name: z.string()
})
.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId } = req.params;
const { spaceId, projectId } = req.query;
const scopeValues = await server.services.appConnection.octopusDeploy.getScopeValues(
connectionId,
spaceId,
projectId,
req.permission
);
if (!scopeValues) {
throw new BadRequestError({ message: "Unable to get Octopus Deploy scope values" });
}
return scopeValues;
}
});
};

View File

@@ -25,6 +25,7 @@ import { registerHumanitecSyncRouter } from "./humanitec-sync-router";
import { registerLaravelForgeSyncRouter } from "./laravel-forge-sync-router";
import { registerNetlifySyncRouter } from "./netlify-sync-router";
import { registerNorthflankSyncRouter } from "./northflank-sync-router";
import { registerOctopusDeploySyncRouter } from "./octopus-deploy-sync-router";
import { registerRailwaySyncRouter } from "./railway-sync-router";
import { registerRenderSyncRouter } from "./render-sync-router";
import { registerSupabaseSyncRouter } from "./supabase-sync-router";
@@ -69,5 +70,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: Fastif
[SecretSync.Northflank]: registerNorthflankSyncRouter,
[SecretSync.Bitbucket]: registerBitbucketSyncRouter,
[SecretSync.LaravelForge]: registerLaravelForgeSyncRouter,
[SecretSync.Chef]: registerChefSyncRouter
[SecretSync.Chef]: registerChefSyncRouter,
[SecretSync.OctopusDeploy]: registerOctopusDeploySyncRouter
};

View File

@@ -0,0 +1,17 @@
import {
CreateOctopusDeploySyncSchema,
OctopusDeploySyncSchema,
UpdateOctopusDeploySyncSchema
} from "@app/services/secret-sync/octopus-deploy";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
export const registerOctopusDeploySyncRouter = async (server: FastifyZodProvider) =>
registerSyncSecretsEndpoints({
destination: SecretSync.OctopusDeploy,
server,
responseSchema: OctopusDeploySyncSchema,
createSchema: CreateOctopusDeploySyncSchema,
updateSchema: UpdateOctopusDeploySyncSchema
});

View File

@@ -48,6 +48,7 @@ import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/
import { LaravelForgeSyncListItemSchema, LaravelForgeSyncSchema } from "@app/services/secret-sync/laravel-forge";
import { NetlifySyncListItemSchema, NetlifySyncSchema } from "@app/services/secret-sync/netlify";
import { NorthflankSyncListItemSchema, NorthflankSyncSchema } from "@app/services/secret-sync/northflank";
import { OctopusDeploySyncListItemSchema, OctopusDeploySyncSchema } from "@app/services/secret-sync/octopus-deploy";
import { RailwaySyncListItemSchema, RailwaySyncSchema } from "@app/services/secret-sync/railway/railway-sync-schemas";
import { RenderSyncListItemSchema, RenderSyncSchema } from "@app/services/secret-sync/render/render-sync-schemas";
import { SupabaseSyncListItemSchema, SupabaseSyncSchema } from "@app/services/secret-sync/supabase";
@@ -90,7 +91,8 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
NorthflankSyncSchema,
BitbucketSyncSchema,
LaravelForgeSyncSchema,
ChefSyncSchema
ChefSyncSchema,
OctopusDeploySyncSchema
]);
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
@@ -126,7 +128,8 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
NorthflankSyncListItemSchema,
BitbucketSyncListItemSchema,
LaravelForgeSyncListItemSchema,
ChefSyncListItemSchema
ChefSyncListItemSchema,
OctopusDeploySyncListItemSchema
]);
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {

View File

@@ -42,7 +42,8 @@ export enum AppConnection {
MongoDB = "mongodb",
LaravelForge = "laravel-forge",
Chef = "chef",
Northflank = "northflank"
Northflank = "northflank",
OctopusDeploy = "octopus-deploy"
}
export enum AWSRegion {

View File

@@ -129,6 +129,11 @@ import {
NorthflankConnectionMethod,
validateNorthflankConnectionCredentials
} from "./northflank";
import {
getOctopusDeployConnectionListItem,
OctopusDeployConnectionMethod,
validateOctopusDeployConnectionCredentials
} from "./octopus-deploy";
import { getOktaConnectionListItem, OktaConnectionMethod, validateOktaConnectionCredentials } from "./okta";
import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres";
import { getRailwayConnectionListItem, validateRailwayConnectionCredentials } from "./railway";
@@ -211,6 +216,7 @@ export const listAppConnectionOptions = (projectType?: ProjectType) => {
getHerokuConnectionListItem(),
getRenderConnectionListItem(),
getLaravelForgeConnectionListItem(),
getOctopusDeployConnectionListItem(),
getFlyioConnectionListItem(),
getGitLabConnectionListItem(),
getCloudflareConnectionListItem(),
@@ -360,7 +366,8 @@ export const validateAppConnectionCredentials = async (
[AppConnection.Okta]: validateOktaConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Chef]: validateChefConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Redis]: validateRedisConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.MongoDB]: validateMongoDBConnectionCredentials as TAppConnectionCredentialsValidator
[AppConnection.MongoDB]: validateMongoDBConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.OctopusDeploy]: validateOctopusDeployConnectionCredentials as TAppConnectionCredentialsValidator
};
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection, gatewayService, gatewayV2Service);
@@ -430,6 +437,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
return "Simple Bind";
case RenderConnectionMethod.ApiKey:
case ChecklyConnectionMethod.ApiKey:
case OctopusDeployConnectionMethod.ApiKey:
return "API Key";
case ChefConnectionMethod.UserKey:
return "User Key";
@@ -510,7 +518,8 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[AppConnection.Redis]: platformManagedCredentialsNotSupported,
[AppConnection.MongoDB]: platformManagedCredentialsNotSupported,
[AppConnection.LaravelForge]: platformManagedCredentialsNotSupported,
[AppConnection.Chef]: platformManagedCredentialsNotSupported
[AppConnection.Chef]: platformManagedCredentialsNotSupported,
[AppConnection.OctopusDeploy]: platformManagedCredentialsNotSupported
};
export const enterpriseAppCheck = async (

View File

@@ -44,7 +44,8 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.Redis]: "Redis",
[AppConnection.MongoDB]: "MongoDB",
[AppConnection.Chef]: "Chef",
[AppConnection.Northflank]: "Northflank"
[AppConnection.Northflank]: "Northflank",
[AppConnection.OctopusDeploy]: "Octopus Deploy"
};
export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanType> = {
@@ -91,5 +92,6 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
[AppConnection.Redis]: AppConnectionPlanType.Regular,
[AppConnection.MongoDB]: AppConnectionPlanType.Regular,
[AppConnection.Chef]: AppConnectionPlanType.Enterprise,
[AppConnection.Northflank]: AppConnectionPlanType.Regular
[AppConnection.Northflank]: AppConnectionPlanType.Regular,
[AppConnection.OctopusDeploy]: AppConnectionPlanType.Regular
};

View File

@@ -103,6 +103,8 @@ import { ValidateNetlifyConnectionCredentialsSchema } from "./netlify";
import { netlifyConnectionService } from "./netlify/netlify-connection-service";
import { ValidateNorthflankConnectionCredentialsSchema } from "./northflank";
import { northflankConnectionService } from "./northflank/northflank-connection-service";
import { ValidateOctopusDeployConnectionCredentialsSchema } from "./octopus-deploy";
import { octopusDeployConnectionService } from "./octopus-deploy/octopus-deploy-connection-service";
import { ValidateOktaConnectionCredentialsSchema } from "./okta";
import { oktaConnectionService } from "./okta/okta-connection-service";
import { ValidatePostgresConnectionCredentialsSchema } from "./postgres";
@@ -182,7 +184,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.Okta]: ValidateOktaConnectionCredentialsSchema,
[AppConnection.Redis]: ValidateRedisConnectionCredentialsSchema,
[AppConnection.MongoDB]: ValidateMongoDBConnectionCredentialsSchema,
[AppConnection.Chef]: ValidateChefConnectionCredentialsSchema
[AppConnection.Chef]: ValidateChefConnectionCredentialsSchema,
[AppConnection.OctopusDeploy]: ValidateOctopusDeployConnectionCredentialsSchema
};
export const appConnectionServiceFactory = ({
@@ -891,6 +894,7 @@ export const appConnectionServiceFactory = ({
northflank: northflankConnectionService(connectAppConnectionById),
okta: oktaConnectionService(connectAppConnectionById),
laravelForge: laravelForgeConnectionService(connectAppConnectionById),
chef: chefConnectionService(connectAppConnectionById, licenseService)
chef: chefConnectionService(connectAppConnectionById, licenseService),
octopusDeploy: octopusDeployConnectionService(connectAppConnectionById)
};
};

View File

@@ -192,6 +192,12 @@ import {
TNorthflankConnectionInput,
TValidateNorthflankConnectionCredentialsSchema
} from "./northflank";
import {
TOctopusDeployConnection,
TOctopusDeployConnectionConfig,
TOctopusDeployConnectionInput,
TValidateOctopusDeployConnectionCredentialsSchema
} from "./octopus-deploy";
import {
TOktaConnection,
TOktaConnectionConfig,
@@ -303,6 +309,7 @@ export type TAppConnection = { id: string } & (
| TRedisConnection
| TMongoDBConnection
| TChefConnection
| TOctopusDeployConnection
);
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
@@ -354,6 +361,7 @@ export type TAppConnectionInput = { id: string } & (
| TRedisConnectionInput
| TMongoDBConnectionInput
| TChefConnectionInput
| TOctopusDeployConnectionInput
);
export type TSqlConnectionInput =
@@ -422,7 +430,8 @@ export type TAppConnectionConfig =
| TOktaConnectionConfig
| TRedisConnectionConfig
| TMongoDBConnectionConfig
| TChefConnectionConfig;
| TChefConnectionConfig
| TOctopusDeployConnectionConfig;
export type TValidateAppConnectionCredentialsSchema =
| TValidateAwsConnectionCredentialsSchema
@@ -468,7 +477,8 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidateOktaConnectionCredentialsSchema
| TValidateRedisConnectionCredentialsSchema
| TValidateMongoDBConnectionCredentialsSchema
| TValidateChefConnectionCredentialsSchema;
| TValidateChefConnectionCredentialsSchema
| TValidateOctopusDeployConnectionCredentialsSchema;
export type TListAwsConnectionKmsKeys = {
connectionId: string;

View File

@@ -0,0 +1,4 @@
export * from "./octopus-deploy-connection-enums";
export * from "./octopus-deploy-connection-fns";
export * from "./octopus-deploy-connection-schemas";
export * from "./octopus-deploy-connection-types";

View File

@@ -0,0 +1,3 @@
export enum OctopusDeployConnectionMethod {
ApiKey = "api-key"
}

View File

@@ -0,0 +1,204 @@
import { AxiosError } from "axios";
import { request } from "@app/lib/config/request";
import { BadRequestError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn";
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
import { AppConnection } from "../app-connection-enums";
import { OctopusDeployConnectionMethod } from "./octopus-deploy-connection-enums";
import {
TOctopusDeployConnection,
TOctopusDeployConnectionConfig,
TOctopusDeployProject,
TOctopusDeployProjectResponse,
TOctopusDeployScopeValues,
TOctopusDeployScopeValuesResponse,
TOctopusDeploySpace,
TOctopusDeploySpaceResponse
} from "./octopus-deploy-connection-types";
export const getOctopusDeployInstanceUrl = async (config: TOctopusDeployConnectionConfig) => {
const instanceUrl = removeTrailingSlash(config.credentials.instanceUrl);
await blockLocalAndPrivateIpAddresses(instanceUrl);
return instanceUrl;
};
export const getOctopusDeployConnectionListItem = () => {
return {
name: "Octopus Deploy" as const,
app: AppConnection.OctopusDeploy as const,
methods: Object.values(OctopusDeployConnectionMethod) as [OctopusDeployConnectionMethod.ApiKey]
};
};
export const validateOctopusDeployConnectionCredentials = async (config: TOctopusDeployConnectionConfig) => {
const instanceUrl = await getOctopusDeployInstanceUrl(config);
const { apiKey } = config.credentials;
try {
await request.get(`${instanceUrl}/api/users/me`, {
headers: {
"X-Octopus-ApiKey": apiKey,
"X-NuGet-ApiKey": apiKey,
Accept: "application/json"
}
});
} catch (error: unknown) {
if (error instanceof AxiosError) {
throw new BadRequestError({
message: `Failed to validate Octopus Deploy credentials: ${error.message || "Unknown error"}`
});
}
throw new BadRequestError({
message: `Failed to validate Octopus Deploy credentials - verify API key is correct`
});
}
return config.credentials;
};
export const getOctopusDeploySpaces = async (
appConnection: TOctopusDeployConnection
): Promise<TOctopusDeploySpace[]> => {
const instanceUrl = await getOctopusDeployInstanceUrl(appConnection);
const { apiKey } = appConnection.credentials;
try {
const { data } = await request.get<TOctopusDeploySpaceResponse[]>(`${instanceUrl}/api/spaces/all`, {
headers: {
"X-Octopus-ApiKey": apiKey,
"X-NuGet-ApiKey": apiKey,
Accept: "application/json"
}
});
return data.map((space) => ({
id: space.Id,
name: space.Name,
slug: space.Slug,
isDefault: space.IsDefault
}));
} catch (error: unknown) {
if (error instanceof AxiosError) {
const errorMessage = (error.response?.data as { error: { ErrorMessage: string } })?.error?.ErrorMessage;
throw new BadRequestError({
message: `Failed to list Octopus Deploy spaces: ${errorMessage || "Unknown error"}`,
error: error.response?.data
});
}
throw new BadRequestError({
message: "Unable to list Octopus Deploy spaces",
error
});
}
};
export const getOctopusDeployProjects = async (
appConnection: TOctopusDeployConnection,
spaceId: string
): Promise<TOctopusDeployProject[]> => {
const instanceUrl = await getOctopusDeployInstanceUrl(appConnection);
const { apiKey } = appConnection.credentials;
try {
const { data } = await request.get<TOctopusDeployProjectResponse[]>(`${instanceUrl}/api/${spaceId}/projects/all`, {
headers: {
"X-Octopus-ApiKey": apiKey,
"X-NuGet-ApiKey": apiKey,
Accept: "application/json"
}
});
return data.map((project) => ({
id: project.Id,
name: project.Name,
slug: project.Slug
}));
} catch (error: unknown) {
if (error instanceof AxiosError) {
const errorMessage = (error.response?.data as { error: { ErrorMessage: string } })?.error?.ErrorMessage;
throw new BadRequestError({
message: `Failed to list Octopus Deploy projects: ${errorMessage || "Unknown error"}`,
error: error.response?.data
});
}
throw new BadRequestError({
message: "Unable to list Octopus Deploy projects",
error
});
}
};
export const getOctopusDeployScopeValues = async (
appConnection: TOctopusDeployConnection,
spaceId: string,
projectId: string
): Promise<TOctopusDeployScopeValues> => {
const instanceUrl = await getOctopusDeployInstanceUrl(appConnection);
const { apiKey } = appConnection.credentials;
try {
const { data } = await request.get<TOctopusDeployScopeValuesResponse>(
`${instanceUrl}/api/${spaceId}/projects/${projectId}/variables`,
{
headers: {
"X-Octopus-ApiKey": apiKey,
"X-NuGet-ApiKey": apiKey,
Accept: "application/json"
}
}
);
const { ScopeValues } = data;
const scopeValues: TOctopusDeployScopeValues = {
environments: ScopeValues.Environments.map((environment) => ({
id: environment.Id,
name: environment.Name
})),
roles: ScopeValues.Roles.map((role) => ({
id: role.Id,
name: role.Name
})),
machines: ScopeValues.Machines.map((machine) => ({
id: machine.Id,
name: machine.Name
})),
processes: ScopeValues.Processes.map((process) => ({
id: process.Id,
name: process.Name
})),
actions: ScopeValues.Actions.map((action) => ({
id: action.Id,
name: action.Name
})),
channels: ScopeValues.Channels.map((channel) => ({
id: channel.Id,
name: channel.Name
}))
};
return scopeValues;
} catch (error: unknown) {
if (error instanceof AxiosError) {
const errorMessage = (error.response?.data as { error: { ErrorMessage: string } })?.error?.ErrorMessage;
throw new BadRequestError({
message: `Failed to get Octopus Deploy scope values: ${errorMessage || "Unknown error"}`,
error: error.response?.data
});
}
throw new BadRequestError({
message: "Unable to get Octopus Deploy scope values",
error
});
}
};

View File

@@ -0,0 +1,72 @@
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 { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { OctopusDeployConnectionMethod } from "./octopus-deploy-connection-enums";
export const OctopusDeployConnectionApiKeyCredentialsSchema = z.object({
instanceUrl: z
.string()
.trim()
.url("Invalid Instance URL")
.min(1, "Instance URL required")
.max(255)
.describe(AppConnections.CREDENTIALS.OCTOPUS_DEPLOY.instanceUrl),
apiKey: z.string().trim().min(1, "API key required").describe(AppConnections.CREDENTIALS.OCTOPUS_DEPLOY.apiKey)
});
const BaseOctopusDeployConnectionSchema = BaseAppConnectionSchema.extend({
app: z.literal(AppConnection.OctopusDeploy)
});
export const OctopusDeployConnectionSchema = z.discriminatedUnion("method", [
BaseOctopusDeployConnectionSchema.extend({
method: z.literal(OctopusDeployConnectionMethod.ApiKey),
credentials: OctopusDeployConnectionApiKeyCredentialsSchema
})
]);
export const SanitizedOctopusDeployConnectionSchema = z.discriminatedUnion("method", [
BaseOctopusDeployConnectionSchema.extend({
method: z.literal(OctopusDeployConnectionMethod.ApiKey),
credentials: OctopusDeployConnectionApiKeyCredentialsSchema.pick({ instanceUrl: true })
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.OctopusDeploy]} (API Key)` }))
]);
export const ValidateOctopusDeployConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z
.literal(OctopusDeployConnectionMethod.ApiKey)
.describe(AppConnections.CREATE(AppConnection.OctopusDeploy).method),
credentials: OctopusDeployConnectionApiKeyCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.OctopusDeploy).credentials
)
})
]);
export const CreateOctopusDeployConnectionSchema = ValidateOctopusDeployConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.OctopusDeploy)
);
export const UpdateOctopusDeployConnectionSchema = z
.object({
credentials: OctopusDeployConnectionApiKeyCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.OctopusDeploy).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.OctopusDeploy));
export const OctopusDeployConnectionListItemSchema = z
.object({
name: z.literal("Octopus Deploy"),
app: z.literal(AppConnection.OctopusDeploy),
methods: z.nativeEnum(OctopusDeployConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.OctopusDeploy] }));

View File

@@ -0,0 +1,65 @@
import { logger } from "@app/lib/logger";
import { OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
getOctopusDeployProjects,
getOctopusDeployScopeValues,
getOctopusDeploySpaces
} from "./octopus-deploy-connection-fns";
import { TOctopusDeployConnection } from "./octopus-deploy-connection-types";
type TGetAppConnectionFunc = (
app: AppConnection,
connectionId: string,
actor: OrgServiceActor
) => Promise<TOctopusDeployConnection>;
export const octopusDeployConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
const listSpaces = async (connectionId: string, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.OctopusDeploy, connectionId, actor);
try {
const spaces = await getOctopusDeploySpaces(appConnection);
return spaces;
} catch (error) {
logger.error({ error, connectionId, actor: actor.type }, "Failed to list Octopus Deploy spaces");
return [];
}
};
const listProjects = async (connectionId: string, spaceId: string, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.OctopusDeploy, connectionId, actor);
try {
const projects = await getOctopusDeployProjects(appConnection, spaceId);
return projects;
} catch (error) {
logger.error({ error, connectionId, spaceId, actor: actor.type }, "Failed to list Octopus Deploy projects");
return [];
}
};
const getScopeValues = async (connectionId: string, spaceId: string, projectId: string, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.OctopusDeploy, connectionId, actor);
try {
const scopeValues = await getOctopusDeployScopeValues(appConnection, spaceId, projectId);
return scopeValues;
} catch (error) {
logger.error(
{ error, connectionId, spaceId, projectId, actor: actor.type },
"Failed to get Octopus Deploy scope values"
);
return null;
}
};
return {
listSpaces,
listProjects,
getScopeValues
};
};

View File

@@ -0,0 +1,69 @@
import z from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import {
CreateOctopusDeployConnectionSchema,
OctopusDeployConnectionSchema,
ValidateOctopusDeployConnectionCredentialsSchema
} from "./octopus-deploy-connection-schemas";
export type TOctopusDeployConnection = z.infer<typeof OctopusDeployConnectionSchema>;
export type TOctopusDeployConnectionInput = z.infer<typeof CreateOctopusDeployConnectionSchema> & {
app: AppConnection.OctopusDeploy;
};
export type TValidateOctopusDeployConnectionCredentialsSchema = typeof ValidateOctopusDeployConnectionCredentialsSchema;
export type TOctopusDeployConnectionConfig = DiscriminativePick<
TOctopusDeployConnectionInput,
"method" | "app" | "credentials"
>;
export type TOctopusDeploySpaceResponse = {
Id: string;
Name: string;
Slug: string;
IsDefault: boolean;
};
export type TOctopusDeploySpace = {
id: string;
name: string;
slug: string;
isDefault: boolean;
};
export type TOctopusDeployProjectResponse = {
Id: string;
Name: string;
Slug: string;
};
export type TOctopusDeployProject = {
id: string;
name: string;
slug: string;
};
export type TOctopusDeployScopeValuesResponse = {
ScopeValues: {
Environments: { Id: string; Name: string }[];
Roles: { Id: string; Name: string }[];
Machines: { Id: string; Name: string }[];
Processes: { Id: string; Name: string }[];
Actions: { Id: string; Name: string }[];
Channels: { Id: string; Name: string }[];
};
};
export type TOctopusDeployScopeValues = {
environments: { id: string; name: string }[];
roles: { id: string; name: string }[];
machines: { id: string; name: string }[];
processes: { id: string; name: string }[];
actions: { id: string; name: string }[];
channels: { id: string; name: string }[];
};

View File

@@ -0,0 +1,4 @@
export * from "./octopus-deploy-sync-constants";
export * from "./octopus-deploy-sync-fns";
export * from "./octopus-deploy-sync-schemas";
export * from "./octopus-deploy-sync-types";

View File

@@ -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 OCTOPUS_DEPLOY_SYNC_LIST_OPTION: TSecretSyncListItem = {
name: "Octopus Deploy",
destination: SecretSync.OctopusDeploy,
connection: AppConnection.OctopusDeploy,
canImportSecrets: false
};

View File

@@ -0,0 +1,151 @@
import { request } from "@app/lib/config/request";
import { BadRequestError } from "@app/lib/errors";
import { getOctopusDeployInstanceUrl } from "@app/services/app-connection/octopus-deploy";
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";
import {
TOctopusDeploySyncWithCredentials,
TOctopusDeployVariable,
TOctopusDeployVariableSet
} from "./octopus-deploy-sync-types";
export const OctopusDeploySyncFns = {
getAuthHeader(apiKey: string): Record<string, string> {
return {
"X-NuGet-ApiKey": apiKey,
"X-Octopus-ApiKey": apiKey,
Accept: "application/json",
"Content-Type": "application/json"
};
},
buildVariableUrl(instanceUrl: string, spaceId: string, projectId: string, scope: string): string {
switch (scope) {
case "project":
return `${instanceUrl}/api/${spaceId}/projects/${projectId}/variables`;
default:
throw new BadRequestError({
message: `Unsupported Octopus Deploy scope: ${scope}`
});
}
},
async syncSecrets(secretSync: TOctopusDeploySyncWithCredentials, secretMap: TSecretMap) {
const {
connection,
environment,
syncOptions: { disableSecretDeletion, keySchema }
} = secretSync;
const instanceUrl = await getOctopusDeployInstanceUrl(connection);
const { apiKey } = connection.credentials;
const { spaceId, projectId, scope } = secretSync.destinationConfig;
const url = this.buildVariableUrl(instanceUrl, spaceId, projectId, scope);
const { data: variableSet } = await request.get<TOctopusDeployVariableSet>(url, {
headers: this.getAuthHeader(apiKey)
});
// Get scope values from destination config (if configured)
const scopeValues = secretSync.destinationConfig.scopeValues || {};
const nonSensitiveVariables: TOctopusDeployVariable[] = [];
let sensitiveVariables: TOctopusDeployVariable[] = [];
variableSet.Variables.forEach((variable) => {
if (!variable.IsSensitive && variable.Type !== "Sensitive") {
nonSensitiveVariables.push(variable);
} else {
// sensitive variables, this could contain infisical secrets
sensitiveVariables.push(variable);
}
});
// Build new variables array from secret map
const newVariables: TOctopusDeployVariable[] = Object.entries(secretMap).map(([key, secret]) => ({
Name: key,
Value: secret.value,
Description: secret.comment || "",
Scope: {
Environment: scopeValues.environments,
Role: scopeValues.roles,
Machine: scopeValues.machines,
ProcessOwner: scopeValues.processes,
Action: scopeValues.actions,
Channel: scopeValues.channels
},
IsEditable: false,
Prompt: null,
Type: "Sensitive",
IsSensitive: true
}));
const keysToDelete = new Set<string>();
if (!disableSecretDeletion) {
sensitiveVariables.forEach((variable) => {
if (!matchesSchema(variable.Name, environment?.slug || "", keySchema)) return;
if (!secretMap[variable.Name]) {
keysToDelete.add(variable.Name);
}
});
}
sensitiveVariables = sensitiveVariables.filter((variable) => !keysToDelete.has(variable.Name));
const newVariableKeys = newVariables.map((variable) => variable.Name);
// Keep sensitive variables that are not in the new variables array, to avoid duplication
sensitiveVariables = sensitiveVariables.filter((variable) => !newVariableKeys.includes(variable.Name));
await request.put(
url,
{
...variableSet,
Variables: [...nonSensitiveVariables, ...sensitiveVariables, ...newVariables]
},
{
headers: this.getAuthHeader(apiKey)
}
);
},
async removeSecrets(secretSync: TOctopusDeploySyncWithCredentials, secretMap: TSecretMap) {
const {
connection,
destinationConfig: { spaceId, projectId, scope }
} = secretSync;
const instanceUrl = await getOctopusDeployInstanceUrl(connection);
const { apiKey } = connection.credentials;
const url = this.buildVariableUrl(instanceUrl, spaceId, projectId, scope);
const { data: variableSet } = await request.get<TOctopusDeployVariableSet>(url, {
headers: this.getAuthHeader(apiKey)
});
const infisicalSecretKeys = Object.keys(secretMap);
const variablesToDelete = variableSet.Variables.filter(
(variable) =>
infisicalSecretKeys.includes(variable.Name) && variable.IsSensitive === true && variable.Type === "Sensitive"
).map((variable) => variable.Id);
await request.put(
url,
{
...variableSet,
Variables: variableSet.Variables.filter((variable) => !variablesToDelete.includes(variable.Id))
},
{
headers: this.getAuthHeader(apiKey)
}
);
},
async getSecrets(secretSync: TOctopusDeploySyncWithCredentials): Promise<TSecretMap> {
throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`);
}
};

View File

@@ -0,0 +1,80 @@
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";
import { SECRET_SYNC_NAME_MAP } from "../secret-sync-maps";
export enum OctopusDeploySyncScope {
Project = "project"
}
const OctopusDeploySyncDestinationConfigBaseSchema = z.object({
spaceId: z.string().min(1, "Space ID is required").describe(SecretSyncs.DESTINATION_CONFIG.OCTOPUS_DEPLOY.spaceId),
spaceName: z.string().optional().describe(SecretSyncs.DESTINATION_CONFIG.OCTOPUS_DEPLOY.spaceName),
scope: z.nativeEnum(OctopusDeploySyncScope).default(OctopusDeploySyncScope.Project)
});
export const OctopusDeploySyncDestinationConfigSchema = z.intersection(
OctopusDeploySyncDestinationConfigBaseSchema,
z.discriminatedUnion("scope", [
z.object({
scope: z.literal(OctopusDeploySyncScope.Project),
projectId: z
.string()
.min(1, "Project ID is required")
.describe(SecretSyncs.DESTINATION_CONFIG.OCTOPUS_DEPLOY.projectId),
projectName: z.string().optional().describe(SecretSyncs.DESTINATION_CONFIG.OCTOPUS_DEPLOY.projectName),
scopeValues: z
.object({
environments: z.array(z.string()).optional(),
roles: z.array(z.string()).optional(),
machines: z.array(z.string()).optional(),
processes: z.array(z.string()).optional(),
actions: z.array(z.string()).optional(),
channels: z.array(z.string()).optional()
})
.optional()
.describe(SecretSyncs.DESTINATION_CONFIG.OCTOPUS_DEPLOY.scopeValues)
})
])
);
const OctopusDeploySyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false };
export const OctopusDeploySyncSchema = BaseSecretSyncSchema(SecretSync.OctopusDeploy, OctopusDeploySyncOptionsConfig)
.extend({
destination: z.literal(SecretSync.OctopusDeploy),
destinationConfig: OctopusDeploySyncDestinationConfigSchema
})
.describe(JSON.stringify({ title: SECRET_SYNC_NAME_MAP[SecretSync.OctopusDeploy] }));
export const CreateOctopusDeploySyncSchema = GenericCreateSecretSyncFieldsSchema(
SecretSync.OctopusDeploy,
OctopusDeploySyncOptionsConfig
).extend({
destinationConfig: OctopusDeploySyncDestinationConfigSchema
});
export const UpdateOctopusDeploySyncSchema = GenericUpdateSecretSyncFieldsSchema(
SecretSync.OctopusDeploy,
OctopusDeploySyncOptionsConfig
).extend({
destinationConfig: OctopusDeploySyncDestinationConfigSchema.optional()
});
export const OctopusDeploySyncListItemSchema = z
.object({
name: z.literal("Octopus Deploy"),
connection: z.literal(AppConnection.OctopusDeploy),
destination: z.literal(SecretSync.OctopusDeploy),
canImportSecrets: z.literal(false)
})
.describe(JSON.stringify({ title: SECRET_SYNC_NAME_MAP[SecretSync.OctopusDeploy] }));

View File

@@ -0,0 +1,67 @@
import z from "zod";
import { TOctopusDeployConnection } from "@app/services/app-connection/octopus-deploy";
import {
CreateOctopusDeploySyncSchema,
OctopusDeploySyncListItemSchema,
OctopusDeploySyncSchema
} from "./octopus-deploy-sync-schemas";
export type TOctopusDeploySyncListItem = z.infer<typeof OctopusDeploySyncListItemSchema>;
export type TOctopusDeploySync = z.infer<typeof OctopusDeploySyncSchema>;
export type TOctopusDeploySyncInput = z.infer<typeof CreateOctopusDeploySyncSchema>;
export type TOctopusDeploySyncWithCredentials = Omit<TOctopusDeploySync, "connection"> & {
connection: TOctopusDeployConnection;
};
export type TOctopusDeployVariable = {
Id?: string;
Name: string;
Value: string;
Description: string;
Scope: {
Environment?: string[];
Machine?: string[];
Role?: string[];
Action?: string[];
Channel?: string[];
ProcessOwner?: string[];
Tenant?: string[];
TenantTag?: string[];
};
IsEditable: boolean;
Prompt: {
Description: string;
DisplaySettings: Record<string, string>;
Label: string;
Required: boolean;
} | null;
Type: "String" | "Sensitive";
IsSensitive: boolean;
};
export type TOctopusDeployVariableSet = {
Id: string;
OwnerId: string;
Version: number;
Variables: TOctopusDeployVariable[];
ScopeValues: {
Environments: { Id: string; Name: string }[];
Machines: { Id: string; Name: string }[];
Actions: { Id: string; Name: string }[];
Roles: { Id: string; Name: string }[];
Channels: { Id: string; Name: string }[];
TenantTags: { Id: string; Name: string }[];
Processes: {
ProcessType: string;
Id: string;
Name: string;
}[];
};
SpaceId: string;
Links: {
Self: string;
};
};

View File

@@ -31,7 +31,8 @@ export enum SecretSync {
Northflank = "northflank",
Bitbucket = "bitbucket",
LaravelForge = "laravel-forge",
Chef = "chef"
Chef = "chef",
OctopusDeploy = "octopus-deploy"
}
export enum SecretSyncInitialSyncBehavior {

View File

@@ -53,6 +53,7 @@ import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns";
import { LARAVEL_FORGE_SYNC_LIST_OPTION, LaravelForgeSyncFns } from "./laravel-forge";
import { NETLIFY_SYNC_LIST_OPTION, NetlifySyncFns } from "./netlify";
import { NORTHFLANK_SYNC_LIST_OPTION, NorthflankSyncFns } from "./northflank";
import { OCTOPUS_DEPLOY_SYNC_LIST_OPTION, OctopusDeploySyncFns } from "./octopus-deploy";
import { RAILWAY_SYNC_LIST_OPTION } from "./railway/railway-sync-constants";
import { RailwaySyncFns } from "./railway/railway-sync-fns";
import { RENDER_SYNC_LIST_OPTION, RenderSyncFns } from "./render";
@@ -97,7 +98,8 @@ const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
[SecretSync.Northflank]: NORTHFLANK_SYNC_LIST_OPTION,
[SecretSync.Bitbucket]: BITBUCKET_SYNC_LIST_OPTION,
[SecretSync.LaravelForge]: LARAVEL_FORGE_SYNC_LIST_OPTION,
[SecretSync.Chef]: CHEF_SYNC_LIST_OPTION
[SecretSync.Chef]: CHEF_SYNC_LIST_OPTION,
[SecretSync.OctopusDeploy]: OCTOPUS_DEPLOY_SYNC_LIST_OPTION
};
export const listSecretSyncOptions = () => {
@@ -289,6 +291,8 @@ export const SecretSyncFns = {
return LaravelForgeSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.Chef:
return ChefSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.OctopusDeploy:
return OctopusDeploySyncFns.syncSecrets(secretSync, schemaSecretMap);
default:
throw new Error(
`Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
@@ -414,6 +418,9 @@ export const SecretSyncFns = {
case SecretSync.Chef:
secretMap = await ChefSyncFns.getSecrets(secretSync);
break;
case SecretSync.OctopusDeploy:
secretMap = await OctopusDeploySyncFns.getSecrets(secretSync);
break;
default:
throw new Error(
`Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
@@ -513,6 +520,8 @@ export const SecretSyncFns = {
return LaravelForgeSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.Chef:
return ChefSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.OctopusDeploy:
return OctopusDeploySyncFns.removeSecrets(secretSync, schemaSecretMap);
default:
throw new Error(
`Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`

View File

@@ -35,7 +35,8 @@ export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
[SecretSync.Northflank]: "Northflank",
[SecretSync.Bitbucket]: "Bitbucket",
[SecretSync.LaravelForge]: "Laravel Forge",
[SecretSync.Chef]: "Chef"
[SecretSync.Chef]: "Chef",
[SecretSync.OctopusDeploy]: "Octopus Deploy"
};
export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
@@ -71,7 +72,8 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
[SecretSync.Northflank]: AppConnection.Northflank,
[SecretSync.Bitbucket]: AppConnection.Bitbucket,
[SecretSync.LaravelForge]: AppConnection.LaravelForge,
[SecretSync.Chef]: AppConnection.Chef
[SecretSync.Chef]: AppConnection.Chef,
[SecretSync.OctopusDeploy]: AppConnection.OctopusDeploy
};
export const SECRET_SYNC_PLAN_MAP: Record<SecretSync, SecretSyncPlanType> = {
@@ -107,7 +109,8 @@ export const SECRET_SYNC_PLAN_MAP: Record<SecretSync, SecretSyncPlanType> = {
[SecretSync.Northflank]: SecretSyncPlanType.Regular,
[SecretSync.Bitbucket]: SecretSyncPlanType.Regular,
[SecretSync.LaravelForge]: SecretSyncPlanType.Regular,
[SecretSync.Chef]: SecretSyncPlanType.Enterprise
[SecretSync.Chef]: SecretSyncPlanType.Enterprise,
[SecretSync.OctopusDeploy]: SecretSyncPlanType.Regular
};
export const SECRET_SYNC_SKIP_FIELDS_MAP: Record<SecretSync, string[]> = {
@@ -152,7 +155,8 @@ export const SECRET_SYNC_SKIP_FIELDS_MAP: Record<SecretSync, string[]> = {
[SecretSync.Northflank]: [],
[SecretSync.Bitbucket]: [],
[SecretSync.LaravelForge]: [],
[SecretSync.Chef]: []
[SecretSync.Chef]: [],
[SecretSync.OctopusDeploy]: []
};
const defaultDuplicateCheck: DestinationDuplicateCheckFn = () => true;
@@ -214,5 +218,6 @@ export const DESTINATION_DUPLICATE_CHECK_MAP: Record<SecretSync, DestinationDupl
[SecretSync.Northflank]: defaultDuplicateCheck,
[SecretSync.Bitbucket]: defaultDuplicateCheck,
[SecretSync.LaravelForge]: defaultDuplicateCheck,
[SecretSync.Chef]: defaultDuplicateCheck
[SecretSync.Chef]: defaultDuplicateCheck,
[SecretSync.OctopusDeploy]: defaultDuplicateCheck
};

View File

@@ -136,6 +136,12 @@ import {
TNorthflankSyncListItem,
TNorthflankSyncWithCredentials
} from "./northflank";
import {
TOctopusDeploySync,
TOctopusDeploySyncInput,
TOctopusDeploySyncListItem,
TOctopusDeploySyncWithCredentials
} from "./octopus-deploy";
import {
TRailwaySync,
TRailwaySyncInput,
@@ -201,7 +207,8 @@ export type TSecretSync =
| TSupabaseSync
| TNetlifySync
| TNorthflankSync
| TBitbucketSync;
| TBitbucketSync
| TOctopusDeploySync;
export type TSecretSyncWithCredentials =
| TAwsParameterStoreSyncWithCredentials
@@ -236,7 +243,8 @@ export type TSecretSyncWithCredentials =
| TNetlifySyncWithCredentials
| TNorthflankSyncWithCredentials
| TBitbucketSyncWithCredentials
| TLaravelForgeSyncWithCredentials;
| TLaravelForgeSyncWithCredentials
| TOctopusDeploySyncWithCredentials;
export type TSecretSyncInput =
| TAwsParameterStoreSyncInput
@@ -271,7 +279,8 @@ export type TSecretSyncInput =
| TNetlifySyncInput
| TNorthflankSyncInput
| TBitbucketSyncInput
| TLaravelForgeSyncInput;
| TLaravelForgeSyncInput
| TOctopusDeploySyncInput;
export type TSecretSyncListItem =
| TAwsParameterStoreSyncListItem
@@ -306,7 +315,8 @@ export type TSecretSyncListItem =
| TDigitalOceanAppPlatformSyncListItem
| TNetlifySyncListItem
| TNorthflankSyncListItem
| TBitbucketSyncListItem;
| TBitbucketSyncListItem
| TOctopusDeploySyncListItem;
export type TSyncOptionsConfig = {
canImportSecrets: boolean;

View File

@@ -0,0 +1,4 @@
---
title: "Available"
openapi: "GET /api/v1/app-connections/octopus-deploy/available"
---

View File

@@ -0,0 +1,10 @@
---
title: "Create"
openapi: "POST /api/v1/app-connections/octopus-deploy"
---
<Note>
Check out the configuration docs for [Octopus Deploy
Connections](/integrations/app-connections/octopus-deploy) to learn how to
obtain the required credentials.
</Note>

View File

@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/app-connections/octopus-deploy/{connectionId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v1/app-connections/octopus-deploy/{connectionId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v1/app-connections/octopus-deploy/connection-name/{connectionName}"
---

View File

@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/app-connections/octopus-deploy"
---

View File

@@ -0,0 +1,10 @@
---
title: "Update"
openapi: "PATCH /api/v1/app-connections/octopus-deploy/{connectionId}"
---
<Note>
Check out the configuration docs for [Octopus Deploy
Connections](/integrations/app-connections/octopus-deploy) to learn how to
obtain the required credentials.
</Note>

View File

@@ -0,0 +1,4 @@
---
title: "Create"
openapi: "POST /api/v1/secret-syncs/octopus-deploy"
---

View File

@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/secret-syncs/octopus-deploy/{syncId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v1/secret-syncs/octopus-deploy/{syncId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v1/secret-syncs/octopus-deploy/sync-name/{syncName}"
---

View File

@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/secret-syncs/octopus-deploy"
---

View File

@@ -0,0 +1,4 @@
---
title: "Remove Secrets"
openapi: "POST /api/v1/secret-syncs/octopus-deploy/{syncId}/remove-secrets"
---

View File

@@ -0,0 +1,4 @@
---
title: "Sync Secrets"
openapi: "POST /api/v1/secret-syncs/octopus-deploy/{syncId}/sync-secrets"
---

View File

@@ -0,0 +1,4 @@
---
title: "Update"
openapi: "PATCH /api/v1/secret-syncs/octopus-deploy/{syncId}"
---

View File

@@ -138,6 +138,7 @@
"integrations/app-connections/netlify",
"integrations/app-connections/northflank",
"integrations/app-connections/oci",
"integrations/app-connections/octopus-deploy",
"integrations/app-connections/okta",
"integrations/app-connections/oracledb",
"integrations/app-connections/postgres",
@@ -566,6 +567,7 @@
"integrations/secret-syncs/netlify",
"integrations/secret-syncs/northflank",
"integrations/secret-syncs/oci-vault",
"integrations/secret-syncs/octopus-deploy",
"integrations/secret-syncs/railway",
"integrations/secret-syncs/render",
"integrations/secret-syncs/supabase",
@@ -1485,6 +1487,18 @@
"api-reference/endpoints/app-connections/oci/delete"
]
},
{
"group": "Octopus Deploy",
"pages": [
"api-reference/endpoints/app-connections/octopus-deploy/list",
"api-reference/endpoints/app-connections/octopus-deploy/available",
"api-reference/endpoints/app-connections/octopus-deploy/get-by-id",
"api-reference/endpoints/app-connections/octopus-deploy/get-by-name",
"api-reference/endpoints/app-connections/octopus-deploy/create",
"api-reference/endpoints/app-connections/octopus-deploy/update",
"api-reference/endpoints/app-connections/octopus-deploy/delete"
]
},
{
"group": "Okta",
"pages": [
@@ -1497,6 +1511,7 @@
"api-reference/endpoints/app-connections/okta/delete"
]
},
{
"group": "OracleDB",
"pages": [
@@ -2396,6 +2411,19 @@
"api-reference/endpoints/secret-syncs/oci-vault/remove-secrets"
]
},
{
"group": "Octopus Deploy",
"pages": [
"api-reference/endpoints/secret-syncs/octopus-deploy/list",
"api-reference/endpoints/secret-syncs/octopus-deploy/get-by-id",
"api-reference/endpoints/secret-syncs/octopus-deploy/get-by-name",
"api-reference/endpoints/secret-syncs/octopus-deploy/create",
"api-reference/endpoints/secret-syncs/octopus-deploy/update",
"api-reference/endpoints/secret-syncs/octopus-deploy/delete",
"api-reference/endpoints/secret-syncs/octopus-deploy/sync-secrets",
"api-reference/endpoints/secret-syncs/octopus-deploy/remove-secrets"
]
},
{
"group": "Railway",
"pages": [

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 492 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 430 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 445 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

View File

@@ -0,0 +1,183 @@
---
title: "Octopus Deploy Connection"
description: "Learn how to configure an Octopus Deploy Connection for Infisical."
---
Infisical supports the use of [API Keys](https://octopus.com/docs/octopus-rest-api/how-to-create-an-api-key) to connect with Octopus Deploy.
## Create Octopus Deploy API Key
Octopus Deploy supports two methods for creating API keys: via a user profile or via a service account.
<Tabs>
<Tab title="Service Account API Key (Recommended)">
<Steps>
<Step title='Navigate to Service Accounts'>
From your Octopus Deploy dashboard, go to **Configuration** > **Users** and click on the **Create Service Accounts** button.
![Service Accounts](/images/app-connections/octopus-deploy/service-account-nav.png)
</Step>
<Step title='Create a new Service Account'>
Provide:
- Username: A name for the service account
- Display Name: A display name for the service account
Then click **Save**.
![Create Service Account](/images/app-connections/octopus-deploy/service-account-create.png)
</Step>
<Step title='Create a Team'>
Navigate to **Configuration** > **Teams** and click **Add Team**.
![Add Team](/images/app-connections/octopus-deploy/add-team.png)
Provide:
- New Team Name: A name for the team
- Team Description(optional): A description for the team
- Select the team access type:
- Accessible in the `current` space only
- Accessible in all spaces(system team)
![Create Team](/images/app-connections/octopus-deploy/create-team.png)
Then click **Save**.
</Step>
<Step title='Add Service Account to Team'>
After creating the team, you will be redirected to the team details page. Click on the **Add Members** button.
![Add Service Account to Team](/images/app-connections/octopus-deploy/team-add-member.png)
Select the service account you created in the previous step and click **Add**.
![Add Service Account to Team](/images/app-connections/octopus-deploy/team-add-member-select.png)
</Step>
<Step title="Add User Role to the team">
After adding the service account to the team, Click on the **User Roles** tab and click **Include User Role** button.
![Add User Role to Team](/images/app-connections/octopus-deploy/team-user-role.png)
Search for the **Project Contributor** role and click on the **Apply** button.
![Apply User Role to Team](/images/app-connections/octopus-deploy/team-apply-user-role.png)
Click on the **Save** button.
![Save User Role to Team](/images/app-connections/octopus-deploy/save-team-settings.png)
</Step>
<Step title='Navigate to the API Keys section'>
After saving the team settings, we have to create an API key for the service account. Go back to **Configuration** > **Users** and find your service account. Click on the service account to view its details.
Click on the **API Keys** section and click **New API Key**.
![Create Service Account API Key](/images/app-connections/octopus-deploy/service-account-api-key.png)
</Step>
<Step title='Generate an API Key'>
Provide a purpose for the key and set an expiry date, then click **Generate New**.
![Generate API Key](/images/app-connections/octopus-deploy/service-account-api-key-generate.png)
</Step>
<Step title='Copy the API Key securely'>
Make sure to copy the API key now, you won't be able to access it again.
![Service Account API Key Generated](/images/app-connections/octopus-deploy/service-account-key-generated.png)
</Step>
</Steps>
</Tab>
<Tab title="User Profile API Key">
<Note>
Infisical recommends using a service account for production integrations as they provide better security and are not tied to individual user accounts.
</Note>
<Steps>
<Step title='Navigate to your user profile'>
From your Octopus Deploy dashboard, click on your profile in the bottom left corner and select **My profile**.
![Octopus Deploy User Profile](/images/app-connections/octopus-deploy/app-connection-profile.png)
</Step>
<Step title='Navigate to the My API Keys section'>
In your profile settings, go to the **My API Keys** tab and click **New API Key**.
![API Keys Tab](/images/app-connections/octopus-deploy/app-connection-api-keys.png)
</Step>
<Step title='Create a new API Key'>
Provide a purpose for the key. Set an expiry date, then click **Generate New**.
![Create API Key](/images/app-connections/octopus-deploy/app-connection-create-key.png)
</Step>
<Step title='Copy the API Key securely'>
Make sure to copy the API key now, you won't be able to access it again.
![API Key Generated](/images/app-connections/octopus-deploy/app-connection-key-generated.png)
</Step>
</Steps>
</Tab>
</Tabs>
## Create an Octopus Deploy Connection in Infisical
<Tabs>
<Tab title="Infisical UI">
<Steps>
<Step title="Navigate to App Connections">
In your Infisical dashboard, navigate to the **App Connections** page in the desired project.
![App Connections Tab](/images/app-connections/general/add-connection.png)
</Step>
<Step title="Select Octopus Deploy Connection">
Click **+ Add Connection** and choose **Octopus Deploy** Connection from the list of integrations.
![Select Octopus Deploy Connection](/images/app-connections/octopus-deploy/app-connection-option.png)
</Step>
<Step title="Fill out the Octopus Deploy Connection form">
Complete the form by providing:
- A descriptive name for the connection
- An optional description
- The Instance URL (e.g., https://your-instance.octopus.app)
- The API Key from the previous step
![Octopus Deploy Connection Modal](/images/app-connections/octopus-deploy/app-connection-form.png)
</Step>
<Step title="Connection created">
After submitting the form, your **Octopus Deploy Connection** will be successfully created and ready to use with your Infisical project.
![Octopus Deploy Connection Created](/images/app-connections/octopus-deploy/app-connection-generated.png)
</Step>
</Steps>
</Tab>
<Tab title="API">
To create an Octopus Deploy Connection via API, send a request to the [Create Octopus Deploy Connection](/api-reference/endpoints/app-connections/octopus-deploy/create) endpoint.
### Sample request
```bash Request
curl --request POST \
--url https://app.infisical.com/api/v1/app-connections/octopus-deploy \
--header 'Content-Type: application/json' \
--data '{
"name": "my-octopus-deploy-connection",
"method": "api-key",
"projectId": "abcdef12-3456-7890-abcd-ef1234567890",
"credentials": {
"instanceUrl": "https://your-instance.octopus.app",
"apiKey": "[API KEY]"
}
}'
```
### Sample response
```json Response
{
"appConnection": {
"id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
"name": "my-octopus-deploy-connection",
"description": null,
"projectId": "abcdef12-3456-7890-abcd-ef1234567890",
"version": 1,
"orgId": "abcdef12-3456-7890-abcd-ef1234567890",
"createdAt": "2025-10-13T10:15:00.000Z",
"updatedAt": "2025-10-13T10:15:00.000Z",
"isPlatformManagedCredentials": false,
"credentialsHash": "d41d8cd98f00b204e9800998ecf8427e",
"app": "octopus-deploy",
"method": "api-key",
"credentials": {
"instanceUrl": "https://your-instance.octopus.app",
}
}
}
```
</Tab>
</Tabs>

View File

@@ -0,0 +1,209 @@
---
title: "Octopus Deploy Sync"
description: "Learn how to configure an Octopus Deploy Sync for Infisical."
---
**Prerequisites:**
- Create an [Octopus Deploy Connection](/integrations/app-connections/octopus-deploy)
<Tabs>
<Tab title="Infisical UI">
<Steps>
<Step title="Add Sync">
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)
</Step>
<Step title="Select 'Octopus Deploy'">
![Select Octopus Deploy](/images/secret-syncs/octopus-deploy/select-option.png)
</Step>
<Step title="Configure source">
Configure the **Source** from where secrets should be retrieved, then click **Next**.
![Configure Source](/images/secret-syncs/octopus-deploy/sync-source.png)
- **Environment**: The project environment to retrieve secrets from.
- **Secret Path**: The folder path to retrieve secrets from.
<Tip>
If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports).
</Tip>
</Step>
<Step title="Configure destination">
Configure the **Destination** to where secrets should be deployed, then click **Next**.
The destination configuration is organized into two tabs:
**General Tab:**
![Configure Destination](/images/secret-syncs/octopus-deploy/sync-destination.png)
- **Octopus Deploy Connection**: The Octopus Deploy Connection to authenticate with.
- **Space**: The Octopus Deploy Space to sync secrets to.
- **Project**: The Octopus Deploy Project within the Space to sync secrets to.
**Advanced Tab:**
![Configure Destination Advanced](/images/secret-syncs/octopus-deploy/sync-destination-advanced.png)
The Advanced tab allows you to specify optional scope values to restrict where the synced variables are available within your Octopus Deploy project:
- **Environments**: Restrict variables to specific environments (e.g., Development, Staging, Production).
- **Target Tags**: Restrict variables to specific target tags (e.g., web-server, database).
- **Targets**: Restrict variables to specific deployment targets.
- **Processes**: Restrict variables to specific deployment processes.
- **Deployment Steps**: Restrict variables to specific deployment steps.
- **Channels**: Restrict variables to specific release channels.
</Step>
<Step title="Configure Sync Options">
Configure the **Sync Options** to specify how secrets should be synced, then click **Next**.
![Configure Options](/images/secret-syncs/octopus-deploy/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.
<Note>
Octopus Deploy does not support importing secrets.
</Note>
- **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.
<Note>
We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched.
</Note>
- **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.
</Step>
<Step title="Configure details">
Configure the **Details** of your Octopus Deploy Sync, then click **Next**.
![Configure Details](/images/secret-syncs/octopus-deploy/sync-details.png)
- **Name**: The name of your sync. Must be slug-friendly.
- **Description**: An optional description for your sync.
</Step>
<Step title="Review configuration">
Review your Octopus Deploy Sync configuration, then click **Create Sync**.
![Review Configuration](/images/secret-syncs/octopus-deploy/sync-review.png)
</Step>
<Step title="Sync created">
If enabled, your Octopus Deploy Sync will begin syncing your secrets to the destination endpoint.
![Sync Created](/images/secret-syncs/octopus-deploy/sync-created.png)
</Step>
</Steps>
</Tab>
<Tab title="API">
To create an **Octopus Deploy Sync**, make an API request to the [Create Octopus Deploy Sync](/api-reference/endpoints/secret-syncs/octopus-deploy/create) API endpoint.
### Sample request
```bash Request
curl --request POST \
--url https://app.infisical.com/api/v1/secret-syncs/octopus-deploy \
--header 'Content-Type: application/json' \
--data '{
"name": "my-octopus-deploy-sync",
"projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"description": "sync to octopus deploy project",
"connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"environment": "dev",
"secretPath": "/",
"isEnabled": true,
"isAutoSyncEnabled": true,
"syncOptions": {
"initialSyncBehavior": "overwrite-destination",
"disableSecretDeletion": false
},
"destinationConfig": {
"spaceId": "Spaces-1",
"scope": "project",
"projectId": "Projects-123",
"scopeValues": {
"environments": ["Environments-1", "Environments-2"],
"roles": ["web-server"],
"channels": ["Channels-1"]
}
}
}'
```
### Sample response
```json Response
{
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "my-octopus-deploy-secret-sync",
"description": null,
"isAutoSyncEnabled": true,
"version": 1,
"projectId": "1e812ad3-e5df-4f1b-839d-13b4ef201840",
"folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"createdAt": "2025-12-12T09:44:59.023Z",
"updatedAt": "2025-12-12T09:44:59.023Z",
"syncStatus": "succeeded",
"lastSyncJobId": null,
"lastSyncMessage": null,
"lastSyncedAt": null,
"importStatus": null,
"lastImportJobId": null,
"lastImportMessage": null,
"lastImportedAt": null,
"removeStatus": null,
"lastRemoveJobId": null,
"lastRemoveMessage": null,
"lastRemovedAt": null,
"syncOptions": {
"initialSyncBehavior": "overwrite-destination",
"disableSecretDeletion": false
},
"connection": {
"app": "octopus-deploy",
"name": "my-octopus-deploy-connection",
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
},
"environment": {
"slug": "dev",
"name": "Development",
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
},
"folder": {
"id": "ad9c26ed-a7ee-41f4-b883-8dd25736052a",
"path": "/"
},
"destination": "octopus-deploy",
"destinationConfig": {
"spaceId": "Spaces-1",
"scope": "project",
"projectId": "Projects-1",
"scopeValues": {
"environments": [
"Environments-1",
"Environments-2"
],
"roles": [
"sample-app-server"
],
"machines": [
"Machines-1",
"Machines-2"
],
"processes": [
"Runbooks-1",
"Runbooks-2"
],
"actions": [
"3c90c3cc-0d44-4b50-8888-8dd25736052a",
"3c90c3cc-0d44-4b50-8888-8dd25736052a"
],
"channels": [
"Channels-2",
"Channels-1"
]
}
}
}
```
</Tab>
</Tabs>

View File

@@ -368,6 +368,13 @@ export const AppConnectionsBrowser = () => {
path: "/integrations/app-connections/mongodb",
description: "Learn how to connect your MongoDB to pull secrets from Infisical.",
category: "Databases"
},
{
name: "Octopus Deploy",
slug: "octopus-deploy",
path: "/integrations/app-connections/octopus-deploy",
description: "Learn how to connect your Octopus Deploy to pull secrets from Infisical.",
category: "DevOps Tools",
}
].sort(function (a, b) {
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());

View File

@@ -39,7 +39,8 @@ export const SecretSyncsBrowser = () => {
{"name": "Zabbix", "slug": "zabbix", "path": "/integrations/secret-syncs/zabbix", "description": "Learn how to sync secrets from Infisical to Zabbix.", "category": "Monitoring"},
{"name": "Laravel Forge", "slug": "laravel-forge", "path": "/integrations/secret-syncs/laravel-forge", "description": "Learn how to sync secrets from Infisical to Laravel Forge.", "category": "Hosting"},
{"name": "Chef", "slug": "chef", "path": "/integrations/secret-syncs/chef", "description": "Learn how to sync secrets from Infisical to Chef.", "category": "DevOps Tools"},
{"name": "Northflank", "slug": "northflank", "path": "/integrations/secret-syncs/northflank", "description": "Learn how to sync secrets from Infisical to Northflank projects.", "category": "Hosting"}
{"name": "Northflank", "slug": "northflank", "path": "/integrations/secret-syncs/northflank", "description": "Learn how to sync secrets from Infisical to Northflank projects.", "category": "Hosting"},
{"name": "Octopus Deploy", "slug": "octopus-deploy", "path": "/integrations/secret-syncs/octopus-deploy", "description": "Learn how to sync secrets from Infisical to Octopus Deploy.", "category": "DevOps Tools"}
].sort(function(a, b) {
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
});

View File

@@ -0,0 +1,460 @@
import { Controller, useFormContext, useWatch } from "react-hook-form";
import { MultiValue, SingleValue } from "react-select";
import { faCircleInfo } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
import {
FilterableSelect,
FormControl,
Select,
SelectItem,
Tab,
TabList,
TabPanel,
Tabs,
Tooltip
} from "@app/components/v2";
import {
useOctopusDeployConnectionGetScopeValues,
useOctopusDeployConnectionListProjects,
useOctopusDeployConnectionListSpaces
} from "@app/hooks/api/appConnections/octopus-deploy/queries";
import {
TOctopusDeployProject,
TOctopusDeploySpace,
TScopeValueOption
} from "@app/hooks/api/appConnections/octopus-deploy/types";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { OctopusDeploySyncScope } from "@app/hooks/api/secretSyncs/types/octopus-deploy-sync";
import { TSecretSyncForm } from "../schemas";
const EMPTY_SCOPE_VALUES = {
environments: [],
roles: [],
processes: [],
actions: [],
machines: [],
channels: []
};
export const OctopusDeploySyncFields = () => {
const { control, setValue } = useFormContext<
TSecretSyncForm & { destination: SecretSync.OctopusDeploy }
>();
const connectionId = useWatch({ name: "connection.id", control });
const spaceId = useWatch({ name: "destinationConfig.spaceId", control });
const scope = useWatch({ name: "destinationConfig.scope", control });
const projectId = useWatch({ name: "destinationConfig.projectId", control });
const { data: spaces = [], isLoading: isSpacesLoading } = useOctopusDeployConnectionListSpaces(
connectionId,
{
enabled: Boolean(connectionId)
}
);
const { data: projects = [], isLoading: isProjectsLoading } =
useOctopusDeployConnectionListProjects(connectionId, spaceId, {
enabled: Boolean(connectionId && spaceId && scope)
});
const { data: scopeValuesData, isLoading: isScopeValuesLoading } =
useOctopusDeployConnectionGetScopeValues(connectionId, spaceId, projectId, {
enabled: Boolean(connectionId && spaceId && projectId && scope)
});
return (
<>
<SecretSyncConnectionField
onChange={() => {
setValue("destinationConfig.spaceId", "");
setValue("destinationConfig.spaceName", "");
setValue("destinationConfig.projectId", "");
setValue("destinationConfig.projectName", "");
setValue("destinationConfig.scopeValues", EMPTY_SCOPE_VALUES);
}}
/>
<Tabs defaultValue="general" className="mt-4">
<TabList className="border-b border-mineshaft-600 bg-mineshaft-800 p-0">
<Tab value="general">General</Tab>
<Tab value="advanced">Advanced</Tab>
</TabList>
<TabPanel value="general">
<div className="space-y-4">
<Controller
name="destinationConfig.spaceId"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Space"
helperText={
<Tooltip
className="max-w-md"
content="Select the Octopus Deploy space where your project is located."
>
<div>
<span>Don&#39;t see the space you&#39;re looking for?</span>{" "}
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
</div>
</Tooltip>
}
>
<FilterableSelect
menuPlacement="top"
isLoading={isSpacesLoading && Boolean(connectionId)}
isDisabled={!connectionId}
value={spaces?.find((space) => space.id === value) ?? null}
onChange={(option) => {
const selectedSpace = option as SingleValue<TOctopusDeploySpace>;
onChange(selectedSpace?.id ?? null);
setValue("destinationConfig.spaceName", selectedSpace?.name ?? "");
setValue("destinationConfig.projectId", "");
setValue("destinationConfig.projectName", "");
setValue("destinationConfig.scopeValues", EMPTY_SCOPE_VALUES);
}}
options={spaces}
placeholder={spaces?.length ? "Select a space..." : "No spaces found..."}
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
/>
</FormControl>
)}
/>
<Controller
name="destinationConfig.scope"
control={control}
defaultValue={OctopusDeploySyncScope.Project}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Scope"
helperText="Select the scope for this sync configuration."
>
<Select
value={value || OctopusDeploySyncScope.Project}
onValueChange={(val) => {
onChange(val);
setValue("destinationConfig.projectId", "");
setValue("destinationConfig.projectName", "");
setValue("destinationConfig.scopeValues", EMPTY_SCOPE_VALUES);
}}
className="w-full border border-mineshaft-500 capitalize"
position="popper"
placeholder="Select a scope..."
dropdownContainerClassName="max-w-none"
>
{Object.values(OctopusDeploySyncScope).map((scopeValue) => (
<SelectItem className="capitalize" value={scopeValue} key={scopeValue}>
{scopeValue}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
{scope === OctopusDeploySyncScope.Project && (
<Controller
name="destinationConfig.projectId"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Project"
helperText={
<Tooltip
className="max-w-md"
content="Ensure the project exists in the selected space."
>
<div>
<span>Don&#39;t see the project you&#39;re looking for?</span>{" "}
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
</div>
</Tooltip>
}
>
<FilterableSelect
menuPlacement="top"
isLoading={isProjectsLoading && Boolean(connectionId && spaceId)}
isDisabled={Boolean(!connectionId || !spaceId)}
value={projects?.find((project) => project.id === value) ?? null}
onChange={(option) => {
const selectedProject = option as SingleValue<TOctopusDeployProject>;
onChange(selectedProject?.id ?? null);
setValue("destinationConfig.projectName", selectedProject?.name ?? "");
setValue("destinationConfig.scopeValues", EMPTY_SCOPE_VALUES);
}}
options={projects}
placeholder={
spaceId && projects?.length ? "Select a project..." : "No projects found..."
}
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
/>
</FormControl>
)}
/>
)}
</div>
</TabPanel>
<TabPanel value="advanced" className="grow">
{scope === OctopusDeploySyncScope.Project && projectId ? (
<div className="max-h-96 overflow-y-auto">
{/* Environments */}
<Controller
name="destinationConfig.scopeValues.environments"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Environments"
isOptional
>
<FilterableSelect
isMulti
menuPlacement="bottom"
menuPosition="absolute"
isLoading={isScopeValuesLoading}
value={
scopeValuesData?.environments?.filter((opt) =>
(value || []).includes(opt.id)
) || []
}
onChange={(options) => {
const selectedIds = (options as MultiValue<TScopeValueOption>).map(
(opt) => opt.id
);
onChange(selectedIds);
}}
options={scopeValuesData?.environments || []}
placeholder={
scopeValuesData?.environments?.length
? "Select environments..."
: "No environments found..."
}
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
/>
</FormControl>
)}
/>
{/* Target Tags */}
<Controller
name="destinationConfig.scopeValues.roles"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Target Tags"
isOptional
>
<FilterableSelect
isMulti
menuPlacement="bottom"
menuPosition="absolute"
isLoading={isScopeValuesLoading}
value={
scopeValuesData?.roles?.filter((opt) => (value || []).includes(opt.id)) ||
[]
}
onChange={(options) => {
const selectedIds = (options as MultiValue<TScopeValueOption>).map(
(opt) => opt.id
);
onChange(selectedIds);
}}
options={scopeValuesData?.roles || []}
placeholder={
scopeValuesData?.roles?.length
? "Select target tags..."
: "No target tags found..."
}
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
/>
</FormControl>
)}
/>
{/* Targets */}
<Controller
name="destinationConfig.scopeValues.machines"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Targets"
isOptional
>
<FilterableSelect
isMulti
menuPlacement="top"
menuPosition="absolute"
isLoading={isScopeValuesLoading}
value={
scopeValuesData?.machines?.filter((opt) =>
(value || []).includes(opt.id)
) || []
}
onChange={(options) => {
const selectedIds = (options as MultiValue<TScopeValueOption>).map(
(opt) => opt.id
);
onChange(selectedIds);
}}
options={scopeValuesData?.machines || []}
placeholder={
scopeValuesData?.machines?.length
? "Select targets..."
: "No targets found..."
}
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
/>
</FormControl>
)}
/>
{/* Processes */}
<Controller
name="destinationConfig.scopeValues.processes"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Processes"
isOptional
>
<FilterableSelect
isMulti
menuPlacement="top"
menuPosition="absolute"
isLoading={isScopeValuesLoading}
value={
scopeValuesData?.processes?.filter((opt) =>
(value || []).includes(opt.id)
) || []
}
onChange={(options) => {
const selectedIds = (options as MultiValue<TScopeValueOption>).map(
(opt) => opt.id
);
onChange(selectedIds);
}}
options={scopeValuesData?.processes || []}
placeholder={
scopeValuesData?.processes?.length
? "Select processes..."
: "No processes found..."
}
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
/>
</FormControl>
)}
/>
{/* Deployment Steps */}
<Controller
name="destinationConfig.scopeValues.actions"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Deployment Steps"
isOptional
>
<FilterableSelect
isMulti
menuPlacement="top"
menuPosition="absolute"
isLoading={isScopeValuesLoading}
value={
scopeValuesData?.actions?.filter((opt) => (value || []).includes(opt.id)) ||
[]
}
onChange={(options) => {
const selectedIds = (options as MultiValue<TScopeValueOption>).map(
(opt) => opt.id
);
onChange(selectedIds);
}}
options={scopeValuesData?.actions || []}
placeholder={
scopeValuesData?.actions?.length
? "Select deployment steps..."
: "No deployment steps found..."
}
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
/>
</FormControl>
)}
/>
{/* Channels */}
<Controller
name="destinationConfig.scopeValues.channels"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Channels"
isOptional
>
<FilterableSelect
isMulti
menuPlacement="top"
menuPosition="absolute"
isLoading={isScopeValuesLoading}
value={
scopeValuesData?.channels?.filter((opt) =>
(value || []).includes(opt.id)
) || []
}
onChange={(options) => {
const selectedIds = (options as MultiValue<TScopeValueOption>).map(
(opt) => opt.id
);
onChange(selectedIds);
}}
options={scopeValuesData?.channels || []}
placeholder={
scopeValuesData?.channels?.length
? "Select channels..."
: "No channels found..."
}
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
/>
</FormControl>
)}
/>
</div>
) : (
<div className="py-8 text-center text-mineshaft-400">
Please select a project in the Config tab to configure scope values.
</div>
)}
</TabPanel>
</Tabs>
</>
);
};

View File

@@ -28,6 +28,7 @@ import { LaravelForgeSyncFields } from "./LaravelForgeSyncFields";
import { NetlifySyncFields } from "./NetlifySyncFields";
import { NorthflankSyncFields } from "./NorthflankSyncFields";
import { OCIVaultSyncFields } from "./OCIVaultSyncFields";
import { OctopusDeploySyncFields } from "./OctopusDeploySyncFields";
import { RailwaySyncFields } from "./RailwaySyncFields";
import { RenderSyncFields } from "./RenderSyncFields";
import { SupabaseSyncFields } from "./SupabaseSyncFields";
@@ -109,6 +110,8 @@ export const SecretSyncDestinationFields = () => {
return <ChefSyncFields />;
case SecretSync.Northflank:
return <NorthflankSyncFields />;
case SecretSync.OctopusDeploy:
return <OctopusDeploySyncFields />;
default:
throw new Error(`Unhandled Destination Config Field: ${destination}`);
}

View File

@@ -76,6 +76,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
case SecretSync.Bitbucket:
case SecretSync.LaravelForge:
case SecretSync.Chef:
case SecretSync.OctopusDeploy:
AdditionalSyncOptionsFieldsComponent = null;
break;
default:

View File

@@ -0,0 +1,92 @@
import { useFormContext } from "react-hook-form";
import { GenericFieldLabel } from "@app/components/secret-syncs";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { useOctopusDeployConnectionGetScopeValues } from "@app/hooks/api/appConnections/octopus-deploy";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { OctopusDeploySyncScope } from "@app/hooks/api/secretSyncs/types/octopus-deploy-sync";
export const OctopusDeploySyncReviewFields = () => {
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.OctopusDeploy }>();
const { spaceName, spaceId, projectId, projectName, scopeValues, scope } =
watch("destinationConfig");
const connectionId = watch("connection.id");
const { data: scopeValuesData } = useOctopusDeployConnectionGetScopeValues(
connectionId,
spaceId,
projectId,
{
enabled: Boolean(connectionId && spaceId && projectId && scope)
}
);
const {
environments = [],
channels = [],
processes = [],
roles = [],
actions = [],
machines = []
} = scopeValues ?? {};
return (
<>
<GenericFieldLabel label="Space">{spaceName || spaceId}</GenericFieldLabel>
<GenericFieldLabel label="Scope" className="capitalize">
{scope}
</GenericFieldLabel>
{scope === OctopusDeploySyncScope.Project && (
<GenericFieldLabel label="Project">{projectName || projectId}</GenericFieldLabel>
)}
{environments.length > 0 && (
<GenericFieldLabel label="Environments">
{scopeValuesData?.environments
.filter((env) => environments.includes(env.id))
.map((env) => env.name)
.join(", ") ?? environments.join(", ")}
</GenericFieldLabel>
)}
{roles.length > 0 && (
<GenericFieldLabel label="Target Tags">
{scopeValuesData?.roles
.filter((role) => roles.includes(role.id))
.map((role) => role.name)
.join(", ") ?? roles.join(", ")}
</GenericFieldLabel>
)}
{machines.length > 0 && (
<GenericFieldLabel label="Targets">
{scopeValuesData?.machines
.filter((machine) => machines.includes(machine.id))
.map((machine) => machine.name)
.join(", ") ?? machines.join(", ")}
</GenericFieldLabel>
)}
{processes.length > 0 && (
<GenericFieldLabel label="Processes">
{scopeValuesData?.processes
.filter((process) => processes.includes(process.id))
.map((process) => process.name)
.join(", ") ?? processes.join(", ")}
</GenericFieldLabel>
)}
{actions.length > 0 && (
<GenericFieldLabel label="Deployment Steps">
{scopeValuesData?.actions
.filter((action) => actions.includes(action.id))
.map((action) => action.name)
.join(", ") ?? actions.join(", ")}
</GenericFieldLabel>
)}
{channels.length > 0 && (
<GenericFieldLabel label="Channels">
{scopeValuesData?.channels
.filter((channel) => channels.includes(channel.id))
.map((channel) => channel.name)
.join(", ") ?? channels.join(", ")}
</GenericFieldLabel>
)}
</>
);
};

View File

@@ -40,6 +40,7 @@ import { LaravelForgeSyncReviewFields } from "./LaravelForgeSyncReviewFields";
import { NetlifySyncReviewFields } from "./NetlifySyncReviewFields";
import { NorthflankSyncReviewFields } from "./NorthflankSyncReviewFields";
import { OCIVaultSyncReviewFields } from "./OCIVaultSyncReviewFields";
import { OctopusDeploySyncReviewFields } from "./OctopusDeploySyncReviewFields";
import { OnePassSyncReviewFields } from "./OnePassSyncReviewFields";
import { RailwaySyncReviewFields } from "./RailwaySyncReviewFields";
import { RenderSyncOptionsReviewFields, RenderSyncReviewFields } from "./RenderSyncReviewFields";
@@ -181,6 +182,9 @@ export const SecretSyncReviewFields = () => {
case SecretSync.Chef:
DestinationFieldsComponent = <ChefSyncReviewFields />;
break;
case SecretSync.OctopusDeploy:
DestinationFieldsComponent = <OctopusDeploySyncReviewFields />;
break;
default:
throw new Error(`Unhandled Destination Review Fields: ${destination}`);
}

View File

@@ -0,0 +1,34 @@
import { z } from "zod";
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { OctopusDeploySyncScope } from "@app/hooks/api/secretSyncs/types/octopus-deploy-sync";
export const OctopusDeploySyncDestinationSchema = BaseSecretSyncSchema().merge(
z.object({
destination: z.literal(SecretSync.OctopusDeploy),
destinationConfig: z.intersection(
z.object({
spaceId: z.string().trim().min(1, { message: "Space ID is required" }),
spaceName: z.string().trim().min(1, { message: "Space Name is required" })
}),
z.discriminatedUnion("scope", [
z.object({
scope: z.literal(OctopusDeploySyncScope.Project),
projectId: z.string().trim().min(1, { message: "Project ID is required" }),
projectName: z.string().trim().min(1, { message: "Project Name is required" }),
scopeValues: z
.object({
environments: z.array(z.string()).optional(),
roles: z.array(z.string()).optional(),
machines: z.array(z.string()).optional(),
processes: z.array(z.string()).optional(),
actions: z.array(z.string()).optional(),
channels: z.array(z.string()).optional()
})
.optional()
})
])
)
})
);

View File

@@ -25,6 +25,7 @@ import { LaravelForgeSyncDestinationSchema } from "./laravel-forge-sync-destinat
import { NetlifySyncDestinationSchema } from "./netlify-sync-destination-schema";
import { NorthflankSyncDestinationSchema } from "./northflank-sync-destination-schema";
import { OCIVaultSyncDestinationSchema } from "./oci-vault-sync-destination-schema";
import { OctopusDeploySyncDestinationSchema } from "./octopus-deploy-sync-destination-schema";
import { RailwaySyncDestinationSchema } from "./railway-sync-destination-schema";
import { RenderSyncDestinationSchema } from "./render-sync-destination-schema";
import { SupabaseSyncDestinationSchema } from "./supabase-sync-destination-schema";
@@ -65,6 +66,7 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
DigitalOceanAppPlatformSyncDestinationSchema,
NetlifySyncDestinationSchema,
NorthflankSyncDestinationSchema,
OctopusDeploySyncDestinationSchema,
BitbucketSyncDestinationSchema,
LaravelForgeSyncDestinationSchema,
ChefSyncDestinationSchema

View File

@@ -34,6 +34,7 @@ import {
MongoDBConnectionMethod,
MsSqlConnectionMethod,
MySqlConnectionMethod,
OctopusDeployConnectionMethod,
OktaConnectionMethod,
OnePassConnectionMethod,
OracleDBConnectionMethod,
@@ -136,7 +137,8 @@ export const APP_CONNECTION_MAP: Record<
image: "Laravel Forge.png",
size: 65
},
[AppConnection.Chef]: { name: "Chef", image: "Chef.png", enterprise: true }
[AppConnection.Chef]: { name: "Chef", image: "Chef.png", enterprise: true },
[AppConnection.OctopusDeploy]: { name: "Octopus Deploy", image: "Octopus Deploy.png" }
};
export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => {
@@ -221,6 +223,8 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
return { name: "Certificate", icon: faCertificate };
case DNSMadeEasyConnectionMethod.APIKeySecret:
return { name: "API Key & Secret", icon: faKey };
case OctopusDeployConnectionMethod.ApiKey:
return { name: "API Key", icon: faKey };
default:
throw new Error(`Unhandled App Connection Method: ${method}`);
}

View File

@@ -125,6 +125,10 @@ export const SECRET_SYNC_MAP: Record<SecretSync, { name: string; image: string }
[SecretSync.Chef]: {
name: "Chef",
image: "Chef.png"
},
[SecretSync.OctopusDeploy]: {
name: "Octopus Deploy",
image: "Octopus Deploy.png"
}
};
@@ -161,7 +165,8 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
[SecretSync.Northflank]: AppConnection.Northflank,
[SecretSync.Bitbucket]: AppConnection.Bitbucket,
[SecretSync.LaravelForge]: AppConnection.LaravelForge,
[SecretSync.Chef]: AppConnection.Chef
[SecretSync.Chef]: AppConnection.Chef,
[SecretSync.OctopusDeploy]: AppConnection.OctopusDeploy
};
export const SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP: Record<

View File

@@ -42,5 +42,6 @@ export enum AppConnection {
Redis = "redis",
MongoDB = "mongodb",
LaravelForge = "laravel-forge",
Chef = "chef"
Chef = "chef",
OctopusDeploy = "octopus-deploy"
}

View File

@@ -0,0 +1,2 @@
export * from "./queries";
export * from "./types";

View File

@@ -0,0 +1,98 @@
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { appConnectionKeys } from "@app/hooks/api/appConnections";
import { TOctopusDeployProject, TOctopusDeployScopeValues, TOctopusDeploySpace } from "./types";
const octopusDeployConnectionKeys = {
all: [...appConnectionKeys.all, "octopus-deploy"] as const,
listSpaces: (connectionId: string) =>
[...octopusDeployConnectionKeys.all, "spaces", connectionId] as const,
listProjects: (connectionId: string, spaceId: string) =>
[...octopusDeployConnectionKeys.all, "projects", connectionId, spaceId] as const,
getScopeValues: (connectionId: string, spaceId: string, projectId: string) =>
[...octopusDeployConnectionKeys.all, "scope-values", connectionId, spaceId, projectId] as const
};
export const useOctopusDeployConnectionListSpaces = (
connectionId: string,
options?: Omit<
UseQueryOptions<
TOctopusDeploySpace[],
unknown,
TOctopusDeploySpace[],
ReturnType<typeof octopusDeployConnectionKeys.listSpaces>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: octopusDeployConnectionKeys.listSpaces(connectionId),
queryFn: async () => {
const { data } = await apiRequest.get<TOctopusDeploySpace[]>(
`/api/v1/app-connections/octopus-deploy/${connectionId}/spaces`
);
return data;
},
...options
});
};
export const useOctopusDeployConnectionListProjects = (
connectionId: string,
spaceId: string,
options?: Omit<
UseQueryOptions<
TOctopusDeployProject[],
unknown,
TOctopusDeployProject[],
ReturnType<typeof octopusDeployConnectionKeys.listProjects>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: octopusDeployConnectionKeys.listProjects(connectionId, spaceId),
queryFn: async () => {
const { data } = await apiRequest.get<TOctopusDeployProject[]>(
`/api/v1/app-connections/octopus-deploy/${connectionId}/projects`,
{
params: { spaceId }
}
);
return data;
},
...options
});
};
export const useOctopusDeployConnectionGetScopeValues = (
connectionId: string,
spaceId: string,
projectId: string,
options?: Omit<
UseQueryOptions<
TOctopusDeployScopeValues,
unknown,
TOctopusDeployScopeValues,
ReturnType<typeof octopusDeployConnectionKeys.getScopeValues>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: octopusDeployConnectionKeys.getScopeValues(connectionId, spaceId, projectId),
queryFn: async () => {
const { data } = await apiRequest.get<TOctopusDeployScopeValues>(
`/api/v1/app-connections/octopus-deploy/${connectionId}/scope-values`,
{ params: { spaceId, projectId } }
);
return data;
},
...options
});
};

View File

@@ -0,0 +1,26 @@
export type TOctopusDeploySpace = {
id: string;
name: string;
slug: string;
isDefault: boolean;
};
export type TOctopusDeployProject = {
id: string;
name: string;
slug: string;
};
export type TOctopusDeployScopeValues = {
environments: { id: string; name: string }[];
roles: { id: string; name: string }[];
machines: { id: string; name: string }[];
processes: { id: string; name: string }[];
actions: { id: string; name: string }[];
channels: { id: string; name: string }[];
};
export type TScopeValueOption = {
id: string;
name: string;
};

View File

@@ -94,6 +94,10 @@ export type THCVaultConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.HCVault;
};
export type TOctopusDeployConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.OctopusDeploy;
};
export type TLdapConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.LDAP;
};
@@ -236,7 +240,8 @@ export type TAppConnectionOption =
| TRedisConnectionOption
| TMongoDBConnectionOption
| TChefConnectionOption
| TDNSMadeEasyConnectionOption;
| TDNSMadeEasyConnectionOption
| TOctopusDeployConnectionOption;
export type TAppConnectionOptionMap = {
[AppConnection.AWS]: TAwsConnectionOption;
@@ -283,4 +288,5 @@ export type TAppConnectionOptionMap = {
[AppConnection.MongoDB]: TMongoDBConnectionOption;
[AppConnection.LaravelForge]: TLaravelForgeConnectionOption;
[AppConnection.Chef]: TChefConnectionOption;
[AppConnection.OctopusDeploy]: TOctopusDeployConnectionOption;
};

View File

@@ -32,6 +32,7 @@ import { TMySqlConnection } from "./mysql-connection";
import { TNetlifyConnection } from "./netlify-connection";
import { TNorthflankConnection } from "./northflank-connection";
import { TOCIConnection } from "./oci-connection";
import { TOctopusDeployConnection } from "./octopus-deploy-connection";
import { TOktaConnection } from "./okta-connection";
import { TOracleDBConnection } from "./oracledb-connection";
import { TPostgresConnection } from "./postgres-connection";
@@ -76,6 +77,7 @@ export * from "./mysql-connection";
export * from "./netlify-connection";
export * from "./northflank-connection";
export * from "./oci-connection";
export * from "./octopus-deploy-connection";
export * from "./okta-connection";
export * from "./oracledb-connection";
export * from "./postgres-connection";
@@ -117,6 +119,7 @@ export type TAppConnection =
| TOnePassConnection
| THerokuConnection
| TLaravelForgeConnection
| TOctopusDeployConnection
| TRenderConnection
| TFlyioConnection
| TGitLabConnection

View File

@@ -0,0 +1,14 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
export enum OctopusDeployConnectionMethod {
ApiKey = "api-key"
}
export type TOctopusDeployConnection = TRootAppConnection & { app: AppConnection.OctopusDeploy } & {
method: OctopusDeployConnectionMethod.ApiKey;
credentials: {
instanceUrl: string;
apiKey: string;
};
};

View File

@@ -31,7 +31,8 @@ export enum SecretSync {
Northflank = "northflank",
Bitbucket = "bitbucket",
LaravelForge = "laravel-forge",
Chef = "chef"
Chef = "chef",
OctopusDeploy = "octopus-deploy"
}
export enum SecretSyncStatus {

View File

@@ -26,6 +26,7 @@ import { TLaravelForgeSync } from "./laravel-forge-sync";
import { TNetlifySync } from "./netlify-sync";
import { TNorthflankSync } from "./northflank-sync";
import { TOCIVaultSync } from "./oci-vault-sync";
import { TOctopusDeploySync } from "./octopus-deploy-sync";
import { TRailwaySync } from "./railway-sync";
import { TRenderSync } from "./render-sync";
import { TSupabaseSync } from "./supabase";
@@ -75,7 +76,8 @@ export type TSecretSync =
| TNorthflankSync
| TBitbucketSync
| TLaravelForgeSync
| TChefSync;
| TChefSync
| TOctopusDeploySync;
export type TListSecretSyncs = { secretSyncs: TSecretSync[] };

View File

@@ -0,0 +1,37 @@
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 enum OctopusDeploySyncScope {
Project = "project"
}
type TOctopusDeploySyncDestinationConfigProject = {
scope: OctopusDeploySyncScope.Project;
projectId: string;
projectName: string;
scopeValues?: {
environments?: string[];
roles?: string[];
machines?: string[];
processes?: string[];
actions?: string[];
channels?: string[];
};
};
type TOctopusDeploySyncDestinationConfig = {
spaceId: string;
spaceName: string;
} & TOctopusDeploySyncDestinationConfigProject;
export type TOctopusDeploySync = TRootSecretSync & {
destination: SecretSync.OctopusDeploy;
destinationConfig: TOctopusDeploySyncDestinationConfig;
connection: {
app: AppConnection.OctopusDeploy;
name: string;
id: string;
};
};

View File

@@ -41,6 +41,7 @@ import { MySqlConnectionForm } from "./MySqlConnectionForm";
import { NetlifyConnectionForm } from "./NetlifyConnectionForm";
import { NorthflankConnectionForm } from "./NorthflankConnectionForm";
import { OCIConnectionForm } from "./OCIConnectionForm";
import { OctopusDeployConnectionForm } from "./OctopusDeployConnectionForm";
import { OktaConnectionForm } from "./OktaConnectionForm";
import { OracleDBConnectionForm } from "./OracleDBConnectionForm";
import { PostgresConnectionForm } from "./PostgresConnectionForm";
@@ -176,6 +177,8 @@ const CreateForm = ({ app, onComplete, projectId }: CreateFormProps) => {
return <RedisConnectionForm onSubmit={onSubmit} />;
case AppConnection.MongoDB:
return <MongoDBConnectionForm onSubmit={onSubmit} />;
case AppConnection.OctopusDeploy:
return <OctopusDeployConnectionForm onSubmit={onSubmit} />;
default:
throw new Error(`Unhandled App ${app}`);
}
@@ -336,6 +339,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
return <RedisConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.MongoDB:
return <MongoDBConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.OctopusDeploy:
return <OctopusDeployConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
default:
throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`);
}

View File

@@ -0,0 +1,158 @@
import { Controller, FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
Button,
FormControl,
Input,
ModalClose,
SecretInput,
Select,
SelectItem
} from "@app/components/v2";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import {
OctopusDeployConnectionMethod,
TOctopusDeployConnection
} from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
} from "./GenericAppConnectionFields";
type Props = {
appConnection?: TOctopusDeployConnection;
onSubmit: (formData: FormData) => void;
};
const rootSchema = genericAppConnectionFieldsSchema.extend({
app: z.literal(AppConnection.OctopusDeploy)
});
const formSchema = z.discriminatedUnion("method", [
rootSchema.extend({
method: z.literal(OctopusDeployConnectionMethod.ApiKey),
credentials: z.object({
instanceUrl: z
.string()
.trim()
.url("Invalid Instance URL")
.min(1, "Instance URL required")
.max(255),
apiKey: z.string().trim().min(1, "API Key required")
})
})
]);
type FormData = z.infer<typeof formSchema>;
export const OctopusDeployConnectionForm = ({ appConnection, onSubmit }: Props) => {
const isUpdate = Boolean(appConnection);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: appConnection ?? {
app: AppConnection.OctopusDeploy,
method: OctopusDeployConnectionMethod.ApiKey
}
});
const {
handleSubmit,
control,
formState: { isSubmitting, isDirty }
} = form;
return (
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
{!isUpdate && <GenericAppConnectionsFields />}
<Controller
name="method"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText={`The method you would like to use to connect with ${
APP_CONNECTION_MAP[AppConnection.OctopusDeploy].name
}. This field cannot be changed after creation.`}
errorText={error?.message}
isError={Boolean(error?.message)}
label="Method"
>
<Select
isDisabled={isUpdate}
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
position="popper"
dropdownContainerClassName="max-w-none"
>
{Object.values(OctopusDeployConnectionMethod).map((method) => {
return (
<SelectItem value={method} key={method}>
{getAppConnectionMethodDetails(method).name}{" "}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
<Controller
name="credentials.instanceUrl"
control={control}
shouldUnregister
render={({ field, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Octopus Deploy Instance URL"
tooltipClassName="max-w-sm"
tooltipText="The URL of the Octopus Deploy Connect Server instance to authenticate with."
>
<Input {...field} placeholder="https://xxxx.octopus.app" />
</FormControl>
)}
/>
<Controller
name="credentials.apiKey"
control={control}
shouldUnregister
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Octopus Deploy API Key"
>
<SecretInput
containerClassName="text-gray-400 group-focus-within:border-primary-400/50! border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
colorSchema="secondary"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
{isUpdate ? "Update Credentials" : "Connect to Octopus Deploy"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
</FormProvider>
);
};

View File

@@ -0,0 +1,14 @@
import { TOctopusDeploySync } from "@app/hooks/api/secretSyncs/types/octopus-deploy-sync";
import { getSecretSyncDestinationColValues } from "../helpers";
import { SecretSyncTableCell } from "../SecretSyncTableCell";
type Props = {
secretSync: TOctopusDeploySync;
};
export const OctopusDeploySyncDestinationCol = ({ secretSync }: Props) => {
const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync);
return <SecretSyncTableCell primaryText={primaryText} secondaryText={secondaryText} />;
};

View File

@@ -25,6 +25,7 @@ import { LaravelForgeSyncDestinationCol } from "./LaravelForgeSyncDestinationCol
import { NetlifySyncDestinationCol } from "./NetlifySyncDestinationCol";
import { NorthflankSyncDestinationCol } from "./NorthflankSyncDestinationCol";
import { OCIVaultSyncDestinationCol } from "./OCIVaultSyncDestinationCol";
import { OctopusDeploySyncDestinationCol } from "./OctopusDeploySyncDestinationCol";
import { RailwaySyncDestinationCol } from "./RailwaySyncDestinationCol";
import { RenderSyncDestinationCol } from "./RenderSyncDestinationCol";
import { SupabaseSyncDestinationCol } from "./SupabaseSyncDestinationCol";
@@ -106,6 +107,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => {
return <LaravelForgeSyncDestinationCol secretSync={secretSync} />;
case SecretSync.Chef:
return <ChefSyncDestinationCol secretSync={secretSync} />;
case SecretSync.OctopusDeploy:
return <OctopusDeploySyncDestinationCol secretSync={secretSync} />;
default:
throw new Error(
`Unhandled Secret Sync Destination Col: ${(secretSync as TSecretSync).destination}`

View File

@@ -8,6 +8,7 @@ import {
} from "@app/hooks/api/secretSyncs/types/github-sync";
import { GitLabSyncScope } from "@app/hooks/api/secretSyncs/types/gitlab-sync";
import { HumanitecSyncScope } from "@app/hooks/api/secretSyncs/types/humanitec-sync";
import { OctopusDeploySyncScope } from "@app/hooks/api/secretSyncs/types/octopus-deploy-sync";
import { RenderSyncScope } from "@app/hooks/api/secretSyncs/types/render-sync";
// This functional ensures parity across what is displayed in the destination column
@@ -206,6 +207,13 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => {
primaryText = destinationConfig.dataBagName;
secondaryText = destinationConfig.dataBagItemName;
break;
case SecretSync.OctopusDeploy:
primaryText = destinationConfig.scope;
if (destinationConfig.scope === OctopusDeploySyncScope.Project) {
primaryText = destinationConfig.projectName || destinationConfig.projectId;
}
secondaryText = destinationConfig.spaceName || destinationConfig.spaceId;
break;
default:
throw new Error(`Unhandled Destination Col Values ${destination}`);
}

View File

@@ -0,0 +1,99 @@
import { GenericFieldLabel } from "@app/components/secret-syncs";
import { useOctopusDeployConnectionGetScopeValues } from "@app/hooks/api/appConnections/octopus-deploy";
import {
OctopusDeploySyncScope,
TOctopusDeploySync
} from "@app/hooks/api/secretSyncs/types/octopus-deploy-sync";
type Props = {
secretSync: TOctopusDeploySync;
};
export const OctopusDeploySyncDestinationSection = ({ secretSync }: Props) => {
const {
destinationConfig: { spaceId, scope, spaceName, scopeValues, projectId, projectName },
connectionId
} = secretSync;
const { data: scopeValuesData, isFetched } = useOctopusDeployConnectionGetScopeValues(
connectionId,
spaceId,
projectId,
{
enabled: Boolean(connectionId && spaceId && projectId && scope)
}
);
const {
environments = [],
channels = [],
processes = [],
roles = [],
actions = [],
machines = []
} = scopeValues ?? {};
return (
<>
<GenericFieldLabel label="Space">{spaceName || spaceId}</GenericFieldLabel>
<GenericFieldLabel label="Scope" className="capitalize">
{scope}
</GenericFieldLabel>
{scope === OctopusDeploySyncScope.Project && (
<GenericFieldLabel label="Project">{projectName || projectId}</GenericFieldLabel>
)}
{isFetched && (
<>
{environments.length > 0 && (
<GenericFieldLabel label="Environments">
{scopeValuesData?.environments
.filter((env) => environments.includes(env.id))
.map((env) => env.name)
.join(", ") ?? environments.join(", ")}
</GenericFieldLabel>
)}
{roles.length > 0 && (
<GenericFieldLabel label="Target Tags">
{scopeValuesData?.roles
.filter((role) => roles.includes(role.id))
.map((role) => role.name)
.join(", ") ?? roles.join(", ")}
</GenericFieldLabel>
)}
{machines.length > 0 && (
<GenericFieldLabel label="Targets">
{scopeValuesData?.machines
.filter((machine) => machines.includes(machine.id))
.map((machine) => machine.name)
.join(", ") ?? machines.join(", ")}
</GenericFieldLabel>
)}
{processes.length > 0 && (
<GenericFieldLabel label="Processes">
{scopeValuesData?.processes
.filter((process) => processes.includes(process.id))
.map((process) => process.name)
.join(", ") ?? processes.join(", ")}
</GenericFieldLabel>
)}
{actions.length > 0 && (
<GenericFieldLabel label="Deployment Steps">
{scopeValuesData?.actions
.filter((action) => actions.includes(action.id))
.map((action) => action.name)
.join(", ") ?? actions.join(", ")}
</GenericFieldLabel>
)}
{channels.length > 0 && (
<GenericFieldLabel label="Channels">
{scopeValuesData?.channels
.filter((channel) => channels.includes(channel.id))
.map((channel) => channel.name)
.join(", ") ?? channels.join(", ")}
</GenericFieldLabel>
)}
</>
)}
</>
);
};

View File

@@ -36,6 +36,7 @@ import { LaravelForgeSyncDestinationSection } from "./LaravelForgeSyncDestinatio
import { NetlifySyncDestinationSection } from "./NetlifySyncDestinationSection";
import { NorthflankSyncDestinationSection } from "./NorthflankSyncDestinationSection";
import { OCIVaultSyncDestinationSection } from "./OCIVaultSyncDestinationSection";
import { OctopusDeploySyncDestinationSection } from "./OctopusDeploySyncDestinationSection";
import { RailwaySyncDestinationSection } from "./RailwaySyncDestinationSection";
import { RenderSyncDestinationSection } from "./RenderSyncDestinationSection";
import { SupabaseSyncDestinationSection } from "./SupabaseSyncDestinationSection";
@@ -160,6 +161,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }:
case SecretSync.Chef:
DestinationComponents = <ChefSyncDestinationSection secretSync={secretSync} />;
break;
case SecretSync.OctopusDeploy:
DestinationComponents = <OctopusDeploySyncDestinationSection secretSync={secretSync} />;
break;
default:
throw new Error(`Unhandled Destination Section components: ${destination}`);
}

Some files were not shown because too many files have changed in this diff Show More