Merge pull request #5019 from Infisical/feat/octopus-deploy-secret-sync
feature: octopus deploy app-connection + secret-sync [ENG-4267]
@@ -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."
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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
|
||||
});
|
||||
@@ -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) => {
|
||||
|
||||
@@ -42,7 +42,8 @@ export enum AppConnection {
|
||||
MongoDB = "mongodb",
|
||||
LaravelForge = "laravel-forge",
|
||||
Chef = "chef",
|
||||
Northflank = "northflank"
|
||||
Northflank = "northflank",
|
||||
OctopusDeploy = "octopus-deploy"
|
||||
}
|
||||
|
||||
export enum AWSRegion {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
@@ -0,0 +1,3 @@
|
||||
export enum OctopusDeployConnectionMethod {
|
||||
ApiKey = "api-key"
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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] }));
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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 }[];
|
||||
};
|
||||
4
backend/src/services/secret-sync/octopus-deploy/index.ts
Normal 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";
|
||||
@@ -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
|
||||
};
|
||||
@@ -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.`);
|
||||
}
|
||||
};
|
||||
@@ -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] }));
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
@@ -31,7 +31,8 @@ export enum SecretSync {
|
||||
Northflank = "northflank",
|
||||
Bitbucket = "bitbucket",
|
||||
LaravelForge = "laravel-forge",
|
||||
Chef = "chef"
|
||||
Chef = "chef",
|
||||
OctopusDeploy = "octopus-deploy"
|
||||
}
|
||||
|
||||
export enum SecretSyncInitialSyncBehavior {
|
||||
|
||||
@@ -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}`
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Available"
|
||||
openapi: "GET /api/v1/app-connections/octopus-deploy/available"
|
||||
---
|
||||
@@ -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>
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Delete"
|
||||
openapi: "DELETE /api/v1/app-connections/octopus-deploy/{connectionId}"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by ID"
|
||||
openapi: "GET /api/v1/app-connections/octopus-deploy/{connectionId}"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by Name"
|
||||
openapi: "GET /api/v1/app-connections/octopus-deploy/connection-name/{connectionName}"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List"
|
||||
openapi: "GET /api/v1/app-connections/octopus-deploy"
|
||||
---
|
||||
@@ -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>
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Create"
|
||||
openapi: "POST /api/v1/secret-syncs/octopus-deploy"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Delete"
|
||||
openapi: "DELETE /api/v1/secret-syncs/octopus-deploy/{syncId}"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by ID"
|
||||
openapi: "GET /api/v1/secret-syncs/octopus-deploy/{syncId}"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by Name"
|
||||
openapi: "GET /api/v1/secret-syncs/octopus-deploy/sync-name/{syncName}"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List"
|
||||
openapi: "GET /api/v1/secret-syncs/octopus-deploy"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Remove Secrets"
|
||||
openapi: "POST /api/v1/secret-syncs/octopus-deploy/{syncId}/remove-secrets"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Sync Secrets"
|
||||
openapi: "POST /api/v1/secret-syncs/octopus-deploy/{syncId}/sync-secrets"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Update"
|
||||
openapi: "PATCH /api/v1/secret-syncs/octopus-deploy/{syncId}"
|
||||
---
|
||||
@@ -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": [
|
||||
|
||||
BIN
docs/images/app-connections/octopus-deploy/add-team.png
Normal file
|
After Width: | Height: | Size: 404 KiB |
|
After Width: | Height: | Size: 304 KiB |
|
After Width: | Height: | Size: 393 KiB |
|
After Width: | Height: | Size: 238 KiB |
|
After Width: | Height: | Size: 170 KiB |
|
After Width: | Height: | Size: 338 KiB |
|
After Width: | Height: | Size: 168 KiB |
|
After Width: | Height: | Size: 494 KiB |
BIN
docs/images/app-connections/octopus-deploy/create-team.png
Normal file
|
After Width: | Height: | Size: 452 KiB |
|
After Width: | Height: | Size: 349 KiB |
|
After Width: | Height: | Size: 492 KiB |
|
After Width: | Height: | Size: 430 KiB |
|
After Width: | Height: | Size: 411 KiB |
|
After Width: | Height: | Size: 456 KiB |
|
After Width: | Height: | Size: 309 KiB |
|
After Width: | Height: | Size: 330 KiB |
BIN
docs/images/app-connections/octopus-deploy/team-add-member.png
Normal file
|
After Width: | Height: | Size: 309 KiB |
|
After Width: | Height: | Size: 445 KiB |
BIN
docs/images/app-connections/octopus-deploy/team-user-role.png
Normal file
|
After Width: | Height: | Size: 303 KiB |
BIN
docs/images/secret-syncs/octopus-deploy/select-option.png
Normal file
|
After Width: | Height: | Size: 171 KiB |
BIN
docs/images/secret-syncs/octopus-deploy/sync-created.png
Normal file
|
After Width: | Height: | Size: 213 KiB |
|
After Width: | Height: | Size: 306 KiB |
BIN
docs/images/secret-syncs/octopus-deploy/sync-destination.png
Normal file
|
After Width: | Height: | Size: 270 KiB |
BIN
docs/images/secret-syncs/octopus-deploy/sync-details.png
Normal file
|
After Width: | Height: | Size: 220 KiB |
BIN
docs/images/secret-syncs/octopus-deploy/sync-options.png
Normal file
|
After Width: | Height: | Size: 270 KiB |
BIN
docs/images/secret-syncs/octopus-deploy/sync-review.png
Normal file
|
After Width: | Height: | Size: 252 KiB |
BIN
docs/images/secret-syncs/octopus-deploy/sync-source.png
Normal file
|
After Width: | Height: | Size: 208 KiB |
183
docs/integrations/app-connections/octopus-deploy.mdx
Normal 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.
|
||||

|
||||
</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**.
|
||||

|
||||
</Step>
|
||||
<Step title='Create a Team'>
|
||||
Navigate to **Configuration** > **Teams** and click **Add Team**.
|
||||
|
||||

|
||||
|
||||
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)
|
||||
|
||||

|
||||
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.
|
||||
|
||||

|
||||
|
||||
Select the service account you created in the previous step and click **Add**.
|
||||

|
||||
</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.
|
||||
|
||||

|
||||
|
||||
Search for the **Project Contributor** role and click on the **Apply** button.
|
||||

|
||||
|
||||
Click on the **Save** button.
|
||||

|
||||
</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**.
|
||||

|
||||
</Step>
|
||||
<Step title='Generate an API Key'>
|
||||
Provide a purpose for the key and set an expiry date, then click **Generate New**.
|
||||

|
||||
</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.
|
||||

|
||||
</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**.
|
||||

|
||||
</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**.
|
||||

|
||||
</Step>
|
||||
<Step title='Create a new API Key'>
|
||||
Provide a purpose for the key. Set an expiry date, then click **Generate New**.
|
||||

|
||||
</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.
|
||||

|
||||
</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.
|
||||

|
||||
</Step>
|
||||
<Step title="Select Octopus Deploy Connection">
|
||||
Click **+ Add Connection** and choose **Octopus Deploy** Connection from the list of integrations.
|
||||

|
||||
</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
|
||||

|
||||
</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.
|
||||

|
||||
</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>
|
||||
209
docs/integrations/secret-syncs/octopus-deploy.mdx
Normal 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.
|
||||
|
||||

|
||||
</Step>
|
||||
<Step title="Select 'Octopus Deploy'">
|
||||

|
||||
</Step>
|
||||
<Step title="Configure source">
|
||||
Configure the **Source** from where secrets should be retrieved, then click **Next**.
|
||||
|
||||

|
||||
|
||||
- **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:**
|
||||

|
||||
- **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:**
|
||||

|
||||
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**.
|
||||
|
||||

|
||||
|
||||
- **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**.
|
||||
|
||||

|
||||
|
||||
- **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**.
|
||||
|
||||

|
||||
</Step>
|
||||
<Step title="Sync created">
|
||||
If enabled, your Octopus Deploy Sync will begin syncing your secrets to the destination endpoint.
|
||||
|
||||

|
||||
</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>
|
||||
@@ -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());
|
||||
|
||||
@@ -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());
|
||||
});
|
||||
|
||||
@@ -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't see the space you'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't see the project you'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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
])
|
||||
)
|
||||
})
|
||||
);
|
||||
@@ -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
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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<
|
||||
|
||||
@@ -42,5 +42,6 @@ export enum AppConnection {
|
||||
Redis = "redis",
|
||||
MongoDB = "mongodb",
|
||||
LaravelForge = "laravel-forge",
|
||||
Chef = "chef"
|
||||
Chef = "chef",
|
||||
OctopusDeploy = "octopus-deploy"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./queries";
|
||||
export * from "./types";
|
||||
@@ -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
|
||||
});
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
@@ -31,7 +31,8 @@ export enum SecretSync {
|
||||
Northflank = "northflank",
|
||||
Bitbucket = "bitbucket",
|
||||
LaravelForge = "laravel-forge",
|
||||
Chef = "chef"
|
||||
Chef = "chef",
|
||||
OctopusDeploy = "octopus-deploy"
|
||||
}
|
||||
|
||||
export enum SecretSyncStatus {
|
||||
|
||||
@@ -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[] };
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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} />;
|
||||
};
|
||||
@@ -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}`
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||