diff --git a/backend/package-lock.json b/backend/package-lock.json index 0d7544314..db16a1574 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -32,6 +32,8 @@ "@octokit/plugin-retry": "^5.0.5", "@octokit/rest": "^20.0.2", "@octokit/webhooks-types": "^7.3.1", + "@octopusdeploy/api-client": "^3.4.1", + "@octopusdeploy/message-contracts": "^1.3.2", "@opentelemetry/api": "^1.9.0", "@opentelemetry/auto-instrumentations-node": "^0.53.0", "@opentelemetry/exporter-metrics-otlp-proto": "^0.55.0", @@ -6944,6 +6946,50 @@ "resolved": "https://registry.npmjs.org/@octokit/webhooks-types/-/webhooks-types-7.1.0.tgz", "integrity": "sha512-y92CpG4kFFtBBjni8LHoV12IegJ+KFxLgKRengrVjKmGE5XMeCuGvlfRe75lTRrgXaG6XIWJlFpIDTlkoJsU8w==" }, + "node_modules/@octopusdeploy/api-client": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@octopusdeploy/api-client/-/api-client-3.4.1.tgz", + "integrity": "sha512-j6FRgDNzc6AQoT3CAguYLWxoMR4W5TKCT1BCPpqjEN9mknmdMSKfYORs3djn/Yj/BhqtITTydDpBoREbzKY5+g==", + "license": "Apache-2.0", + "dependencies": { + "adm-zip": "^0.5.9", + "axios": "^1.2.1", + "form-data": "^4.0.0", + "glob": "^8.0.3", + "lodash": "^4.17.21", + "semver": "^7.3.8", + "urijs": "^1.19.11" + } + }, + "node_modules/@octopusdeploy/message-contracts": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@octopusdeploy/message-contracts/-/message-contracts-1.3.2.tgz", + "integrity": "sha512-UcpfKgnDVgejse07AvcvmBZCmoKkmXxdegneysS3+d/ariTySXzkLNaO/pGku50TBUPq8HnOZJgr7q9JP+rgsA==", + "license": "https://github.com/OctopusDeploy/message-contracts.ts/blob/main/LICENSE", + "dependencies": { + "@octopusdeploy/runtime-inputs": "^0.16.0", + "lodash": "^4.17.21", + "moment": "^2.29.4" + } + }, + "node_modules/@octopusdeploy/runtime-inputs": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@octopusdeploy/runtime-inputs/-/runtime-inputs-0.16.0.tgz", + "integrity": "sha512-U0S7OmvyLxFgjzi1ePAf715b62o1NGBfxmDU+p3mljsC8xg4vnJGg7vJVQyleIPQQM7wKlY0sE+XPYgRhPzesg==", + "license": "Apache-2.0", + "dependencies": { + "@octopusdeploy/step-inputs": "^0.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@octopusdeploy/step-inputs": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@octopusdeploy/step-inputs/-/step-inputs-0.6.0.tgz", + "integrity": "sha512-paj0VXsu3kAVLqQU2+UwDH0b1wdoW8/1TnCtnkXFu9LH9t4tyKCLoDblTQw2LvKR/u6NGSfUR2V5DdpLFyRpow==", + "license": "Apache-2.0" + }, "node_modules/@opentelemetry/api": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", @@ -22365,6 +22411,12 @@ "punycode": "^2.1.0" } }, + "node_modules/urijs": { + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz", + "integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==", + "license": "MIT" + }, "node_modules/url": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", diff --git a/backend/package.json b/backend/package.json index 82b2df024..316018cd1 100644 --- a/backend/package.json +++ b/backend/package.json @@ -140,6 +140,8 @@ "@octokit/plugin-retry": "^5.0.5", "@octokit/rest": "^20.0.2", "@octokit/webhooks-types": "^7.3.1", + "@octopusdeploy/api-client": "^3.4.1", + "@octopusdeploy/message-contracts": "^1.3.2", "@opentelemetry/api": "^1.9.0", "@opentelemetry/auto-instrumentations-node": "^0.53.0", "@opentelemetry/exporter-metrics-otlp-proto": "^0.55.0", diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 136a4db29..0a4c65ac1 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1080,7 +1080,8 @@ export const INTEGRATION = { shouldDisableDelete: "The flag to disable deletion of secrets in AWS Parameter Store.", shouldMaskSecrets: "Specifies if the secrets synced from Infisical to Gitlab should be marked as 'Masked'.", shouldProtectSecrets: "Specifies if the secrets synced from Infisical to Gitlab should be marked as 'Protected'.", - shouldEnableDelete: "The flag to enable deletion of secrets." + shouldEnableDelete: "The flag to enable deletion of secrets.", + octopusDeployScopeValues: "Specifies the scope values to set on synced secrets to Octopus Deploy." } }, UPDATE: { diff --git a/backend/src/server/routes/v1/integration-auth-router.ts b/backend/src/server/routes/v1/integration-auth-router.ts index dae37ddb3..575544cc7 100644 --- a/backend/src/server/routes/v1/integration-auth-router.ts +++ b/backend/src/server/routes/v1/integration-auth-router.ts @@ -5,6 +5,7 @@ import { INTEGRATION_AUTH } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; +import { OctopusDeployScope } from "@app/services/integration-auth/integration-auth-types"; import { integrationAuthPubSchema } from "../sanitizedSchemas"; @@ -1008,4 +1009,118 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) return { buildConfigs }; } }); + + server.route({ + method: "GET", + url: "/:integrationAuthId/octopus-deploy/scope-values", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + params: z.object({ + integrationAuthId: z.string().trim() + }), + querystring: z.object({ + scope: z.nativeEnum(OctopusDeployScope), + spaceId: z.string().trim(), + resourceId: z.string().trim() + }), + response: { + 200: z.object({ + Environments: z + .object({ + Name: z.string(), + Id: z.string() + }) + .array(), + Machines: z + .object({ + Name: z.string(), + Id: z.string() + }) + .array(), + Actions: z + .object({ + Name: z.string(), + Id: z.string() + }) + .array(), + Roles: z + .object({ + Name: z.string(), + Id: z.string() + }) + .array(), + Channels: z + .object({ + Name: z.string(), + Id: z.string() + }) + .array(), + TenantTags: z + .object({ + Name: z.string(), + Id: z.string() + }) + .array(), + Processes: z + .object({ + ProcessType: z.string(), + Name: z.string(), + Id: z.string() + }) + .array() + }) + } + }, + handler: async (req) => { + const scopeValues = await server.services.integrationAuth.getOctopusDeployScopeValues({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.params.integrationAuthId, + scope: req.query.scope, + spaceId: req.query.spaceId, + resourceId: req.query.resourceId + }); + return scopeValues; + } + }); + + server.route({ + method: "GET", + url: "/:integrationAuthId/octopus-deploy/spaces", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + params: z.object({ + integrationAuthId: z.string().trim() + }), + response: { + 200: z.object({ + spaces: z + .object({ + Name: z.string(), + Id: z.string(), + IsDefault: z.boolean() + }) + .array() + }) + } + }, + handler: async (req) => { + const spaces = await server.services.integrationAuth.getOctopusDeploySpaces({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.params.integrationAuthId + }); + return { spaces }; + } + }); }; diff --git a/backend/src/services/integration-auth/integration-app-list.ts b/backend/src/services/integration-auth/integration-app-list.ts index 8fe1231bb..3b8078cc1 100644 --- a/backend/src/services/integration-auth/integration-app-list.ts +++ b/backend/src/services/integration-auth/integration-app-list.ts @@ -1,6 +1,7 @@ /* eslint-disable no-await-in-loop */ import { createAppAuth } from "@octokit/auth-app"; import { Octokit } from "@octokit/rest"; +import { Client as OctopusDeployClient, ProjectRepository as OctopusDeployRepository } from "@octopusdeploy/api-client"; import { TIntegrationAuths } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; @@ -1087,6 +1088,33 @@ const getAppsAzureDevOps = async ({ accessToken, orgName }: { accessToken: strin return apps; }; +const getAppsOctopusDeploy = async ({ + apiKey, + instanceURL, + spaceName = "Default" +}: { + apiKey: string; + instanceURL: string; + spaceName?: string; +}) => { + const client = await OctopusDeployClient.create({ + instanceURL, + apiKey, + userAgentApp: "Infisical Integration" + }); + + const repository = new OctopusDeployRepository(client, spaceName); + + const projects = await repository.list({ + take: 1000 + }); + + return projects.Items.map((project) => ({ + name: project.Name, + appId: project.Id + })); +}; + export const getApps = async ({ integration, integrationAuth, @@ -1260,6 +1288,13 @@ export const getApps = async ({ orgName: azureDevOpsOrgName as string }); + case Integrations.OCTOPUS_DEPLOY: + return getAppsOctopusDeploy({ + apiKey: accessToken, + instanceURL: url!, + spaceName: workspaceSlug + }); + default: throw new NotFoundError({ message: `Integration '${integration}' not found` }); } diff --git a/backend/src/services/integration-auth/integration-auth-service.ts b/backend/src/services/integration-auth/integration-auth-service.ts index 636f634a3..be1a8d53c 100644 --- a/backend/src/services/integration-auth/integration-auth-service.ts +++ b/backend/src/services/integration-auth/integration-auth-service.ts @@ -1,6 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import { createAppAuth } from "@octokit/auth-app"; import { Octokit } from "@octokit/rest"; +import { Client as OctopusClient, SpaceRepository as OctopusSpaceRepository } from "@octopusdeploy/api-client"; import AWS from "aws-sdk"; import { SecretEncryptionAlgo, SecretKeyEncoding, TIntegrationAuths, TIntegrationAuthsInsert } from "@app/db/schemas"; @@ -9,7 +10,7 @@ import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services import { getConfig } from "@app/lib/config/env"; import { request } from "@app/lib/config/request"; import { decryptSymmetric128BitHexKeyUTF8, encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; -import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors"; import { TGenericPermission, TProjectPermission } from "@app/lib/types"; import { TIntegrationDALFactory } from "../integration/integration-dal"; @@ -20,6 +21,7 @@ import { getApps } from "./integration-app-list"; import { TIntegrationAuthDALFactory } from "./integration-auth-dal"; import { IntegrationAuthMetadataSchema, TIntegrationAuthMetadata } from "./integration-auth-schema"; import { + OctopusDeployScope, TBitbucketEnvironment, TBitbucketWorkspace, TChecklyGroups, @@ -38,6 +40,8 @@ import { TIntegrationAuthGithubOrgsDTO, TIntegrationAuthHerokuPipelinesDTO, TIntegrationAuthNorthflankSecretGroupDTO, + TIntegrationAuthOctopusDeployProjectScopeValuesDTO, + TIntegrationAuthOctopusDeploySpacesDTO, TIntegrationAuthQoveryEnvironmentsDTO, TIntegrationAuthQoveryOrgsDTO, TIntegrationAuthQoveryProjectDTO, @@ -48,6 +52,7 @@ import { TIntegrationAuthVercelBranchesDTO, TNorthflankSecretGroup, TOauthExchangeDTO, + TOctopusDeployVariableSet, TSaveIntegrationAccessTokenDTO, TTeamCityBuildConfig, TVercelBranches @@ -1521,6 +1526,88 @@ export const integrationAuthServiceFactory = ({ return integrationAuthDAL.create(newIntegrationAuth); }; + const getOctopusDeploySpaces = async ({ + actorId, + actor, + actorOrgId, + actorAuthMethod, + id + }: TIntegrationAuthOctopusDeploySpacesDTO) => { + const integrationAuth = await integrationAuthDAL.findById(id); + if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${id}' not found` }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + integrationAuth.projectId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); + const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); + const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey); + + const client = await OctopusClient.create({ + apiKey: accessToken, + instanceURL: integrationAuth.url!, + userAgentApp: "Infisical Integration" + }); + + const spaceRepository = new OctopusSpaceRepository(client); + + const spaces = await spaceRepository.list({ + partialName: "", // throws error if no string is present... + take: 1000 + }); + + return spaces.Items; + }; + + const getOctopusDeployScopeValues = async ({ + actorId, + actor, + actorOrgId, + actorAuthMethod, + id, + scope, + spaceId, + resourceId + }: TIntegrationAuthOctopusDeployProjectScopeValuesDTO) => { + const integrationAuth = await integrationAuthDAL.findById(id); + if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${id}' not found` }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + integrationAuth.projectId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); + const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); + const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey); + + let url: string; + switch (scope) { + case OctopusDeployScope.Project: + url = `${integrationAuth.url}/api/${spaceId}/projects/${resourceId}/variables`; + break; + // future support tenant, variable set etc. + default: + throw new InternalServerError({ message: `Unhandled Octopus Deploy scope` }); + } + + // SDK doesn't support variable set... + const { data: variableSet } = await request.get(url, { + headers: { + "X-NuGet-ApiKey": accessToken, + Accept: "application/json" + } + }); + + return variableSet.ScopeValues; + }; + return { listIntegrationAuthByProjectId, listOrgIntegrationAuth, @@ -1552,6 +1639,8 @@ export const integrationAuthServiceFactory = ({ getBitbucketWorkspaces, getBitbucketEnvironments, getIntegrationAccessToken, - duplicateIntegrationAuth + duplicateIntegrationAuth, + getOctopusDeploySpaces, + getOctopusDeployScopeValues }; }; diff --git a/backend/src/services/integration-auth/integration-auth-types.ts b/backend/src/services/integration-auth/integration-auth-types.ts index 0b92c29f1..80e8d6c36 100644 --- a/backend/src/services/integration-auth/integration-auth-types.ts +++ b/backend/src/services/integration-auth/integration-auth-types.ts @@ -193,3 +193,72 @@ export type TIntegrationsWithEnvironment = TIntegrations & { | null | undefined; }; + +export type TIntegrationAuthOctopusDeploySpacesDTO = { + id: string; +} & Omit; + +export type TIntegrationAuthOctopusDeployProjectScopeValuesDTO = { + id: string; + spaceId: string; + resourceId: string; + scope: OctopusDeployScope; +} & Omit; + +export enum OctopusDeployScope { + Project = "project" + // add tenant, variable set, etc. +} + +export type TOctopusDeployVariableSet = { + Id: string; + OwnerId: string; + Version: number; + Variables: { + Id: string; + Name: string; + Value: string; + Description: string; + Scope: { + Environment?: string[]; + Machine?: string[]; + Role?: string[]; + TargetRole?: string[]; + Action?: string[]; + User?: string[]; + Trigger?: string[]; + ParentDeployment?: string[]; + Private?: string[]; + Channel?: string[]; + TenantTag?: string[]; + Tenant?: string[]; + ProcessOwner?: string[]; + }; + IsEditable: boolean; + Prompt: { + Description: string; + DisplaySettings: Record; + Label: string; + Required: boolean; + } | null; + Type: "String"; + IsSensitive: boolean; + }[]; + 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; + }; +}; diff --git a/backend/src/services/integration-auth/integration-list.ts b/backend/src/services/integration-auth/integration-list.ts index 007fccd52..45cbdaea9 100644 --- a/backend/src/services/integration-auth/integration-list.ts +++ b/backend/src/services/integration-auth/integration-list.ts @@ -34,7 +34,8 @@ export enum Integrations { HASURA_CLOUD = "hasura-cloud", RUNDECK = "rundeck", AZURE_DEVOPS = "azure-devops", - AZURE_APP_CONFIGURATION = "azure-app-configuration" + AZURE_APP_CONFIGURATION = "azure-app-configuration", + OCTOPUS_DEPLOY = "octopus-deploy" } export enum IntegrationType { @@ -413,6 +414,15 @@ export const getIntegrationOptions = async () => { type: "pat", clientId: "", docsLink: "" + }, + { + name: "Octopus Deploy", + slug: "octopus-deploy", + image: "Octopus Deploy.png", + isAvailable: true, + type: "sat", + clientId: "", + docsLink: "" } ]; diff --git a/backend/src/services/integration-auth/integration-sync-secret.ts b/backend/src/services/integration-auth/integration-sync-secret.ts index 39b045288..3983bd8bc 100644 --- a/backend/src/services/integration-auth/integration-sync-secret.ts +++ b/backend/src/services/integration-auth/integration-sync-secret.ts @@ -32,14 +32,14 @@ import { z } from "zod"; import { SecretType, TIntegrationAuths, TIntegrations } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { request } from "@app/lib/config/request"; -import { BadRequestError } from "@app/lib/errors"; +import { BadRequestError, InternalServerError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { TCreateManySecretsRawFn, TUpdateManySecretsRawFn } from "@app/services/secret/secret-types"; import { TIntegrationDALFactory } from "../integration/integration-dal"; import { IntegrationMetadataSchema } from "../integration/integration-schema"; import { IntegrationAuthMetadataSchema } from "./integration-auth-schema"; -import { TIntegrationsWithEnvironment } from "./integration-auth-types"; +import { OctopusDeployScope, TIntegrationsWithEnvironment, TOctopusDeployVariableSet } from "./integration-auth-types"; import { IntegrationInitialSyncBehavior, IntegrationMappingBehavior, @@ -4201,6 +4201,61 @@ const syncSecretsRundeck = async ({ } }; +const syncSecretsOctopusDeploy = async ({ + integration, + integrationAuth, + secrets, + accessToken +}: { + integration: TIntegrations; + integrationAuth: TIntegrationAuths; + secrets: Record; + accessToken: string; +}) => { + let url: string; + switch (integration.scope) { + case OctopusDeployScope.Project: + url = `${integrationAuth.url}/api/${integration.targetEnvironmentId}/projects/${integration.appId}/variables`; + break; + // future support tenant, variable set, etc. + default: + throw new InternalServerError({ message: `Unhandled Octopus Deploy scope: ${integration.scope}` }); + } + + // SDK doesn't support variable set... + const { data: variableSet } = await request.get(url, { + headers: { + "X-NuGet-ApiKey": accessToken, + Accept: "application/json" + } + }); + + await request.put( + url, + { + ...variableSet, + Variables: Object.entries(secrets).map(([key, value]) => ({ + Name: key, + Value: value.value, + Description: value.comment ?? "", + Scope: + (integration.metadata as { octopusDeployScopeValues: TOctopusDeployVariableSet["ScopeValues"] }) + ?.octopusDeployScopeValues ?? {}, + IsEditable: false, + Prompt: null, + Type: "String", + IsSensitive: true + })) + } as unknown as TOctopusDeployVariableSet, + { + headers: { + "X-NuGet-ApiKey": accessToken, + Accept: "application/json" + } + } + ); +}; + /** * Sync/push [secrets] to [app] in integration named [integration] * @@ -4513,6 +4568,14 @@ export const syncIntegrationSecrets = async ({ accessToken }); break; + case Integrations.OCTOPUS_DEPLOY: + await syncSecretsOctopusDeploy({ + integration, + integrationAuth, + secrets, + accessToken + }); + break; default: throw new BadRequestError({ message: "Invalid integration" }); } diff --git a/backend/src/services/integration/integration-schema.ts b/backend/src/services/integration/integration-schema.ts index 99f1d996f..d047a0c11 100644 --- a/backend/src/services/integration/integration-schema.ts +++ b/backend/src/services/integration/integration-schema.ts @@ -46,5 +46,18 @@ export const IntegrationMetadataSchema = z.object({ shouldDisableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldDisableDelete), shouldEnableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldEnableDelete), shouldMaskSecrets: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldMaskSecrets), - shouldProtectSecrets: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldProtectSecrets) + shouldProtectSecrets: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldProtectSecrets), + + octopusDeployScopeValues: z + .object({ + // in Octopus Deploy Scope Value Format + Environment: z.string().array().optional(), + Action: z.string().array().optional(), + Channel: z.string().array().optional(), + Machine: z.string().array().optional(), + ProcessOwner: z.string().array().optional(), + Role: z.string().array().optional() + }) + .optional() + .describe(INTEGRATION.CREATE.metadata.octopusDeployScopeValues) }); diff --git a/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-add-role.png b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-add-role.png new file mode 100644 index 000000000..e929b3fb7 Binary files /dev/null and b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-add-role.png differ diff --git a/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-add-to-team.png b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-add-to-team.png new file mode 100644 index 000000000..7c9a6e4ea Binary files /dev/null and b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-add-to-team.png differ diff --git a/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-authorize.png b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-authorize.png new file mode 100644 index 000000000..455285047 Binary files /dev/null and b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-authorize.png differ diff --git a/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-copy-api-key.png b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-copy-api-key.png new file mode 100644 index 000000000..adb372216 Binary files /dev/null and b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-copy-api-key.png differ diff --git a/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-create-api-key.png b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-create-api-key.png new file mode 100644 index 000000000..f19c77e7e Binary files /dev/null and b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-create-api-key.png differ diff --git a/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-create-service-account.png b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-create-service-account.png new file mode 100644 index 000000000..3708f8517 Binary files /dev/null and b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-create-service-account.png differ diff --git a/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-create-team.png b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-create-team.png new file mode 100644 index 000000000..9cb703e12 Binary files /dev/null and b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-create-team.png differ diff --git a/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-create.png b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-create.png new file mode 100644 index 000000000..e71fef6ee Binary files /dev/null and b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-create.png differ diff --git a/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-generate-api-key.png b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-generate-api-key.png new file mode 100644 index 000000000..57c61e079 Binary files /dev/null and b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-generate-api-key.png differ diff --git a/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-integrations.png b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-integrations.png new file mode 100644 index 000000000..35b18e0e3 Binary files /dev/null and b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-integrations.png differ diff --git a/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-save-team.png b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-save-team.png new file mode 100644 index 000000000..34106b71f Binary files /dev/null and b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-save-team.png differ diff --git a/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-sync.png b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-sync.png new file mode 100644 index 000000000..c3ceca1ba Binary files /dev/null and b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-sync.png differ diff --git a/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-team-settings.png b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-team-settings.png new file mode 100644 index 000000000..a53229e25 Binary files /dev/null and b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-team-settings.png differ diff --git a/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-user-settings.png b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-user-settings.png new file mode 100644 index 000000000..227b2770b Binary files /dev/null and b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-user-settings.png differ diff --git a/docs/integrations/cicd/octopus-deploy.mdx b/docs/integrations/cicd/octopus-deploy.mdx new file mode 100644 index 000000000..6da2cf472 --- /dev/null +++ b/docs/integrations/cicd/octopus-deploy.mdx @@ -0,0 +1,69 @@ +--- +title: "Octopus Deploy" +description: "Learn how to sync secrets from Infisical to Octopus Deploy" +--- + +Prerequisites: + +- Set up and add secrets to [Infisical Cloud](https://app.infisical.com) + + + + Navigate to **Configuration** > **Users** and click on the **Create Service Account** button. + + ![integrations octopus deploy + users](/images/integrations/octopus-deploy/integrations-octopus-deploy-user-settings.png) + + Fill out the required fields and click on the **Save** button. + ![integrations octopus deploy service + account](/images/integrations/octopus-deploy/integrations-octopus-deploy-create-service-account.png) + + + On the **Service Account** user page, expand the **API Keys** section and click on the **New API Key** button. + + ![integrations octopus deploy + new api key](/images/integrations/octopus-deploy/integrations-octopus-deploy-create-api-key.png) + + Fill out the required fields and click on the **Generate New** button. + + ![integrations octopus deploy + generate api key](/images/integrations/octopus-deploy/integrations-octopus-deploy-generate-api-key.png) + + Copy the generated **API Key** and click on the **Close** button. + + ![integrations octopus deploy + copy api key](/images/integrations/octopus-deploy/integrations-octopus-deploy-copy-api-key.png) + + + Navigate to **Configuration** > **Teams** and click on the **Add Team** button. + + ![integrations octopus deploy + teams](/images/integrations/octopus-deploy/integrations-octopus-deploy-team-settings.png) + + Create a new team for **Service Accounts** and click on the **Save** button. + ![integrations octopus deploy add + team](/images/integrations/octopus-deploy/integrations-octopus-deploy-create-team.png) + + On the **Members** tab, click on the **Add Member** button, add your **Infisical Service Account** and click on the **Add** button. + ![integrations octopus deploy add service account to team](/images/integrations/octopus-deploy/integrations-octopus-deploy-add-to-team.png) + + On the **User Roles** tab, click on the **Include User Role** button, add the **Project Contributor** role and click on the **Apply** button. + ![integrations octopus deploy add user roles to team](/images/integrations/octopus-deploy/integrations-octopus-deploy-add-role.png) + + Save your team changes by clicking on the **Save** button. + ![integrations octopus deploy save team changes](/images/integrations/octopus-deploy/integrations-octopus-deploy-save-team.png) + + + In Infisical, navigate to your **Project** > **Integrations** page and select the **Octopus Deploy** integration. + ![integration octopus deploy](/images/integrations/octopus-deploy/integrations-octopus-deploy-integrations.png) + + Enter your **Instance URL** and **API Key** from **Octopus Deploy** to authorize Infisical. + ![integration octopus deploy](/images/integrations/octopus-deploy/integrations-octopus-deploy-authorize.png) + + Select a **Space** and **Project** from **Octopus Deploy** to sync secrets to; configuring additional **Scope Values** as needed. Click on the **Create Integration** button once configured. + ![integration octopus deploy](/images/integrations/octopus-deploy/integrations-octopus-deploy-create.png) + + Your Infisical secrets will begin to sync to **Octopus Deploy**. + ![integration octopus deploy](/images/integrations/octopus-deploy/integrations-octopus-deploy-sync.png) + + \ No newline at end of file diff --git a/docs/integrations/overview.mdx b/docs/integrations/overview.mdx index f138ee8ec..08debf8cf 100644 --- a/docs/integrations/overview.mdx +++ b/docs/integrations/overview.mdx @@ -42,6 +42,7 @@ Missing an integration? [Throw in a request](https://github.com/Infisical/infisi | [CircleCI](/integrations/cicd/circleci) | CI/CD | Available | | [Travis CI](/integrations/cicd/travisci) | CI/CD | Available | | [Rundeck](/integrations/cicd/rundeck) | CI/CD | Available | +| [Octopus Deploy](/integrations/cicd/octopus-deploy) | CI/CD | Available | | [React](/integrations/frameworks/react) | Framework | Available | | [Vue](/integrations/frameworks/vue) | Framework | Available | | [Express](/integrations/frameworks/express) | Framework | Available | diff --git a/docs/mint.json b/docs/mint.json index e4e487915..be7064873 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -422,7 +422,8 @@ "integrations/cicd/travisci", "integrations/cicd/rundeck", "integrations/cicd/codefresh", - "integrations/cloud/checkly" + "integrations/cloud/checkly", + "integrations/cicd/octopus-deploy" ] } ] diff --git a/frontend/public/data/frequentConstants.ts b/frontend/public/data/frequentConstants.ts index 1a536e522..51c006424 100644 --- a/frontend/public/data/frequentConstants.ts +++ b/frontend/public/data/frequentConstants.ts @@ -36,7 +36,8 @@ const integrationSlugNameMapping: Mapping = { "hasura-cloud": "Hasura Cloud", rundeck: "Rundeck", "azure-devops": "Azure DevOps", - "azure-app-configuration": "Azure App Configuration" + "azure-app-configuration": "Azure App Configuration", + "octopus-deploy": "Octopus Deploy" }; const envMapping: Mapping = { diff --git a/frontend/public/images/integrations/Octopus Deploy.png b/frontend/public/images/integrations/Octopus Deploy.png new file mode 100644 index 000000000..a7a0d2ffa Binary files /dev/null and b/frontend/public/images/integrations/Octopus Deploy.png differ diff --git a/frontend/src/components/v2/FilterableSelect/FilterableSelect.tsx b/frontend/src/components/v2/FilterableSelect/FilterableSelect.tsx index 5333d79d2..b90fc6ac4 100644 --- a/frontend/src/components/v2/FilterableSelect/FilterableSelect.tsx +++ b/frontend/src/components/v2/FilterableSelect/FilterableSelect.tsx @@ -79,7 +79,8 @@ export const FilterableSelect = ({ isMulti, closeMenuOnSelect, ...props }: P ), placeholder: () => "text-mineshaft-400 text-sm pl-1 py-0.5", input: () => "pl-1 py-0.5", - valueContainer: () => `p-1 max-h-[14rem] ${isMulti ? "!overflow-y-scroll" : ""} gap-1`, + valueContainer: () => + `p-1 max-h-[14rem] ${isMulti ? "!overflow-y-auto thin-scrollbar" : ""} gap-1`, singleValue: () => "leading-7 ml-1", multiValue: () => "bg-mineshaft-600 rounded items-center py-0.5 px-2 gap-1.5", multiValueLabel: () => "leading-6 text-sm", diff --git a/frontend/src/hooks/api/integrationAuth/queries.tsx b/frontend/src/hooks/api/integrationAuth/queries.tsx index a018578ba..f61365c9d 100644 --- a/frontend/src/hooks/api/integrationAuth/queries.tsx +++ b/frontend/src/hooks/api/integrationAuth/queries.tsx @@ -17,7 +17,9 @@ import { Project, Service, Team, - TeamCityBuildConfig + TeamCityBuildConfig, + TGetIntegrationAuthOctopusDeployScopeValuesDTO, + TOctopusDeployScopeValues } from "./types"; const integrationAuthKeys = { @@ -119,7 +121,14 @@ const integrationAuthKeys = { }: { integrationAuthId: string; appId: string; - }) => [{ integrationAuthId, appId }, "integrationAuthTeamCityBranchConfigs"] as const + }) => [{ integrationAuthId, appId }, "integrationAuthTeamCityBranchConfigs"] as const, + getIntegrationAuthOctopusDeploySpaces: (integrationAuthId: string) => + [{ integrationAuthId }, "getIntegrationAuthOctopusDeploySpaces"] as const, + getIntegrationAuthOctopusDeployScopeValues: ({ + integrationAuthId, + ...params + }: TGetIntegrationAuthOctopusDeployScopeValuesDTO) => + [{ integrationAuthId }, "getIntegrationAuthOctopusDeployScopeValues", params] as const }; const fetchIntegrationAuthById = async (integrationAuthId: string) => { @@ -479,6 +488,28 @@ const fetchIntegrationAuthTeamCityBuildConfigs = async ({ return buildConfigs; }; +const fetchIntegrationAuthOctopusDeploySpaces = async (integrationAuthId: string) => { + const { + data: { spaces } + } = await apiRequest.get<{ + spaces: { Name: string; Slug: string; Id: string; IsDefault: boolean }[]; + }>(`/api/v1/integration-auth/${integrationAuthId}/octopus-deploy/spaces`); + return spaces; +}; + +const fetchIntegrationAuthOctopusDeployScopeValues = async ({ + integrationAuthId, + scope, + spaceId, + resourceId +}: TGetIntegrationAuthOctopusDeployScopeValuesDTO) => { + const { data } = await apiRequest.get( + `/api/v1/integration-auth/${integrationAuthId}/octopus-deploy/scope-values`, + { params: { scope, spaceId, resourceId } } + ); + return data; +}; + export const useGetIntegrationAuthById = (integrationAuthId: string) => { return useQuery({ queryKey: integrationAuthKeys.getIntegrationAuthById(integrationAuthId), @@ -487,17 +518,24 @@ export const useGetIntegrationAuthById = (integrationAuthId: string) => { }); }; -export const useGetIntegrationAuthApps = ({ - integrationAuthId, - teamId, - azureDevOpsOrgName, - workspaceSlug -}: { - integrationAuthId: string; - teamId?: string; - azureDevOpsOrgName?: string; - workspaceSlug?: string; -}) => { +export const useGetIntegrationAuthApps = ( + { + integrationAuthId, + teamId, + azureDevOpsOrgName, + workspaceSlug + }: { + integrationAuthId: string; + teamId?: string; + azureDevOpsOrgName?: string; + workspaceSlug?: string; + }, + options?: UseQueryOptions< + Awaited>, + unknown, + Awaited> + > +) => { return useQuery({ queryKey: integrationAuthKeys.getIntegrationAuthApps(integrationAuthId, teamId, workspaceSlug), queryFn: () => @@ -507,7 +545,7 @@ export const useGetIntegrationAuthApps = ({ azureDevOpsOrgName, workspaceSlug }), - enabled: true + ...options }); }; @@ -759,6 +797,23 @@ export const useGetIntegrationAuthBitBucketWorkspaces = (integrationAuthId: stri }); }; +export const useGetIntegrationAuthOctopusDeploySpaces = (integrationAuthId: string) => { + return useQuery({ + queryKey: integrationAuthKeys.getIntegrationAuthBitBucketWorkspaces(integrationAuthId), + queryFn: () => fetchIntegrationAuthOctopusDeploySpaces(integrationAuthId) + }); +}; + +export const useGetIntegrationAuthOctopusDeployScopeValues = ( + params: TGetIntegrationAuthOctopusDeployScopeValuesDTO, + options?: UseQueryOptions +) => + useQuery({ + queryKey: integrationAuthKeys.getIntegrationAuthOctopusDeployScopeValues(params), + queryFn: () => fetchIntegrationAuthOctopusDeployScopeValues(params), + ...options + }); + export const useGetIntegrationAuthBitBucketEnvironments = ( { integrationAuthId, diff --git a/frontend/src/hooks/api/integrationAuth/types.ts b/frontend/src/hooks/api/integrationAuth/types.ts index ec24ce844..a2b22020d 100644 --- a/frontend/src/hooks/api/integrationAuth/types.ts +++ b/frontend/src/hooks/api/integrationAuth/types.ts @@ -99,3 +99,29 @@ export type TDuplicateIntegrationAuthDTO = { integrationAuthId: string; projectId: string; }; + +export enum OctopusDeployScope { + Project = "project" + // tenant, variable set +} + +export type TGetIntegrationAuthOctopusDeployScopeValuesDTO = { + integrationAuthId: string; + spaceId: string; + resourceId: string; + scope: OctopusDeployScope; +}; + +export type TOctopusDeployScopeValues = { + 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; + }[]; +}; diff --git a/frontend/src/hooks/api/integrations/queries.tsx b/frontend/src/hooks/api/integrations/queries.tsx index 56131f641..c68e22197 100644 --- a/frontend/src/hooks/api/integrations/queries.tsx +++ b/frontend/src/hooks/api/integrations/queries.tsx @@ -87,6 +87,14 @@ export const useCreateIntegration = () => { shouldMaskSecrets?: boolean; shouldProtectSecrets?: boolean; shouldEnableDelete?: boolean; + octopusDeployScopeValues?: { + Environment?: string[]; + Action?: string[]; + Channel?: string[]; + Machine?: string[]; + ProcessOwner?: string[]; + Role?: string[]; + }; }; }) => { const { diff --git a/frontend/src/pages/integrations/bitbucket/create.tsx b/frontend/src/pages/integrations/bitbucket/create.tsx index d641253dc..5151e6ab0 100644 --- a/frontend/src/pages/integrations/bitbucket/create.tsx +++ b/frontend/src/pages/integrations/bitbucket/create.tsx @@ -181,7 +181,7 @@ export default function BitBucketCreateIntegrationPage() { onChange={onChange} options={currentWorkspace?.environments} placeholder="Select a project environment" - isDisabled={!bitbucketWorkspaces?.length} + isDisabled={!currentWorkspace?.environments.length} /> )} diff --git a/frontend/src/pages/integrations/octopus-deploy/authorize.tsx b/frontend/src/pages/integrations/octopus-deploy/authorize.tsx new file mode 100644 index 000000000..b07e9c159 --- /dev/null +++ b/frontend/src/pages/integrations/octopus-deploy/authorize.tsx @@ -0,0 +1,138 @@ +import { Controller, useForm } from "react-hook-form"; +import Head from "next/head"; +import Image from "next/image"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2"; +import { useWorkspace } from "@app/context"; +import { removeTrailingSlash } from "@app/helpers/string"; +import { useSaveIntegrationAccessToken } from "@app/hooks/api"; + +const formSchema = z.object({ + instanceUrl: z.string().min(1, { message: "Instance URL required" }), + apiKey: z.string().min(1, { message: "API Key required" }) +}); + +type TForm = z.infer; + +export default function OctopusDeployIntegrationPage() { + const router = useRouter(); + const { mutateAsync, isLoading } = useSaveIntegrationAccessToken(); + const { currentWorkspace } = useWorkspace(); + + const { control, handleSubmit } = useForm({ + resolver: zodResolver(formSchema) + }); + + const onSubmit = async ({ instanceUrl, apiKey }: TForm) => { + try { + const integrationAuth = await mutateAsync({ + workspaceId: currentWorkspace!.id, + integration: "octopus-deploy", + url: removeTrailingSlash(instanceUrl), + accessToken: apiKey + }); + + router.push(`/integrations/octopus-deploy/create?integrationAuthId=${integrationAuth.id}`); + } catch (err: any) { + createNotification({ + type: "error", + text: err.message ?? "Error authorizing integration" + }); + console.error(err); + } + }; + + return ( +
+ + Authorize Octopus Deploy Integration + + + + +
+
+ Databricks logo +
+ Octopus Deploy Integration + + +
+ + Docs + +
+
+ +
+
+ ( + + + + )} + name="instanceUrl" + control={control} + /> + ( + + + + )} + name="apiKey" + control={control} + /> + +
+
+ ); +} + +OctopusDeployIntegrationPage.requireAuth = true; diff --git a/frontend/src/pages/integrations/octopus-deploy/create.tsx b/frontend/src/pages/integrations/octopus-deploy/create.tsx new file mode 100644 index 000000000..60f8503c0 --- /dev/null +++ b/frontend/src/pages/integrations/octopus-deploy/create.tsx @@ -0,0 +1,443 @@ +import { useEffect } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { SiOctopusdeploy } from "react-icons/si"; +import { useRouter } from "next/router"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { + Button, + Card, + CardTitle, + FilterableSelect, + FormControl, + Input, + Spinner +} from "@app/components/v2"; +import { useWorkspace } from "@app/context"; +import { useCreateIntegration, useGetIntegrationAuthApps } from "@app/hooks/api"; +import { + useGetIntegrationAuthOctopusDeployScopeValues, + useGetIntegrationAuthOctopusDeploySpaces +} from "@app/hooks/api/integrationAuth/queries"; +import { OctopusDeployScope } from "@app/hooks/api/integrationAuth/types"; + +const formSchema = z.object({ + scope: z.nativeEnum(OctopusDeployScope), + secretPath: z.string().default("/"), + sourceEnvironment: z.object({ name: z.string(), slug: z.string() }), + targetSpace: z.object({ Name: z.string(), Id: z.string() }), + targetResource: z.object({ appId: z.string().optional(), name: z.string() }), + targetEnvironments: z.object({ Name: z.string(), Id: z.string() }).array().optional(), + targetRoles: z.object({ Name: z.string(), Id: z.string() }).array().optional(), + targetMachines: z.object({ Name: z.string(), Id: z.string() }).array().optional(), + targetProcesses: z + .object({ Name: z.string(), Id: z.string(), ProcessType: z.string() }) + .array() + .optional(), + targetActions: z.object({ Name: z.string(), Id: z.string() }).array().optional(), + targetChannels: z.object({ Name: z.string(), Id: z.string() }).array().optional() +}); + +type TFormData = z.infer; + +export default function OctopusDeployCreateIntegrationPage() { + const router = useRouter(); + const createIntegration = useCreateIntegration(); + + const { watch, control, reset, handleSubmit } = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + secretPath: "/", + scope: OctopusDeployScope.Project + } + }); + + const integrationAuthId = router.query.integrationAuthId as string; + + const { currentWorkspace, isLoading: isProjectLoading } = useWorkspace(); + + const { data: octopusDeploySpaces, isLoading: isLoadingOctopusDeploySpaces } = + useGetIntegrationAuthOctopusDeploySpaces((integrationAuthId as string) ?? ""); + + const currentSpace = watch("targetSpace", octopusDeploySpaces?.[0]); + const currentScope = watch("scope"); + + const { data: octopusDeployResources, isLoading: isOctopusDeployResourcesLoading } = + useGetIntegrationAuthApps( + { + integrationAuthId, + workspaceSlug: currentSpace?.Name + // scope once we support other resources than project + }, + { + enabled: Boolean(currentSpace ?? octopusDeploySpaces?.find((space) => space.IsDefault)) + } + ); + + const currentResource = watch("targetResource", octopusDeployResources?.[0]); + + const { data: octopusDeployScopeValues, isLoading: isOctopusDeployScopeValuesLoading } = + useGetIntegrationAuthOctopusDeployScopeValues( + { + integrationAuthId, + spaceId: currentSpace?.Id, + resourceId: currentResource?.appId!, + scope: currentScope + }, + { enabled: Boolean(currentSpace && currentResource) } + ); + + const onSubmit = async ({ + sourceEnvironment, + secretPath, + targetEnvironments, + targetResource, + targetSpace, + targetChannels, + targetActions, + targetMachines, + targetProcesses, + targetRoles, + scope + }: TFormData) => { + try { + await createIntegration.mutateAsync({ + integrationAuthId, + isActive: true, + scope, + app: targetResource.name, + appId: targetResource.appId, + targetEnvironment: targetSpace.Name, + targetEnvironmentId: targetSpace.Id, + metadata: { + octopusDeployScopeValues: { + Environment: targetEnvironments?.map(({ Id }) => Id), + Action: targetActions?.map(({ Id }) => Id), + Channel: targetChannels?.map(({ Id }) => Id), + ProcessOwner: targetProcesses?.map(({ Id }) => Id), + Role: targetRoles?.map(({ Id }) => Id), + Machine: targetMachines?.map(({ Id }) => Id) + } + }, + sourceEnvironment: sourceEnvironment.slug, + secretPath + }); + + createNotification({ + type: "success", + text: "Successfully created integration" + }); + router.push(`/integrations/${currentWorkspace?.id}`); + } catch (err) { + createNotification({ + type: "error", + text: "Failed to create integration" + }); + console.error(err); + } + }; + + useEffect(() => { + if (!octopusDeployResources || !octopusDeploySpaces || !currentWorkspace) return; + + reset({ + targetResource: octopusDeployResources[0], + targetSpace: octopusDeploySpaces.find((space) => space.IsDefault), + sourceEnvironment: currentWorkspace.environments[0], + secretPath: "/", + scope: OctopusDeployScope.Project + }); + }, [octopusDeploySpaces, octopusDeployResources, currentWorkspace]); + + if (isProjectLoading || isLoadingOctopusDeploySpaces || isOctopusDeployResourcesLoading) + return ( +
+ +
+ ); + + return ( +
+ + + + Octopus Deploy Integration + +
+ ( + + option.slug} + value={value} + getOptionLabel={(option) => option.name} + onChange={onChange} + options={currentWorkspace?.environments} + placeholder="Select a project environment" + isDisabled={!currentWorkspace?.environments.length} + /> + + )} + /> + ( + + + + )} + /> +
+
+ Sync To +
+
+ ( + + option.Id} + value={value} + getOptionLabel={(option) => option.Name} + onChange={onChange} + options={octopusDeploySpaces} + placeholder={ + octopusDeploySpaces?.length ? "Select a space..." : "No spaces found..." + } + isDisabled={!octopusDeploySpaces?.length} + /> + + )} + /> + ( + + option.appId!} + value={value} + getOptionLabel={(option) => option.name} + onChange={onChange} + options={octopusDeployResources} + placeholder={ + octopusDeployResources?.length ? "Select a project..." : "No projects found..." + } + isDisabled={!octopusDeployResources?.length} + /> + + )} + /> + + ( + + option.Name} + value={value} + getOptionLabel={(option) => option.Name} + onChange={onChange} + isLoading={isOctopusDeployScopeValuesLoading} + options={octopusDeployScopeValues?.Environments} + placeholder={ + octopusDeployScopeValues?.Environments?.length + ? "Select environments..." + : "No environments found..." + } + isDisabled={!octopusDeployScopeValues?.Environments?.length} + /> + + )} + /> + ( + + option.Name} + value={value} + getOptionLabel={(option) => option.Name} + onChange={onChange} + isLoading={isOctopusDeployScopeValuesLoading} + options={octopusDeployScopeValues?.Roles} + placeholder={ + octopusDeployScopeValues?.Roles?.length + ? "Select target tags..." + : "No target tags found..." + } + isDisabled={!octopusDeployScopeValues?.Roles?.length} + /> + + )} + /> + ( + + option.Name} + value={value} + getOptionLabel={(option) => option.Name} + onChange={onChange} + isLoading={isOctopusDeployScopeValuesLoading} + options={octopusDeployScopeValues?.Machines} + placeholder={ + octopusDeployScopeValues?.Machines?.length + ? "Select targets..." + : "No targets found..." + } + isDisabled={!octopusDeployScopeValues?.Machines?.length} + /> + + )} + /> + ( + + option.Name} + value={value} + getOptionLabel={(option) => option.Name} + onChange={onChange} + isLoading={isOctopusDeployScopeValuesLoading} + options={octopusDeployScopeValues?.Processes} + placeholder={ + octopusDeployScopeValues?.Processes?.length + ? "Select processes..." + : "No processes found..." + } + isDisabled={!octopusDeployScopeValues?.Processes?.length} + /> + + )} + /> + ( + + option.Name} + value={value} + getOptionLabel={(option) => option.Name} + onChange={onChange} + isLoading={isOctopusDeployScopeValuesLoading} + options={octopusDeployScopeValues?.Actions} + placeholder={ + octopusDeployScopeValues?.Actions?.length + ? "Select deployment steps..." + : "No deployment steps found..." + } + isDisabled={!octopusDeployScopeValues?.Actions?.length} + /> + + )} + /> + ( + + option.Name} + value={value} + getOptionLabel={(option) => option.Name} + onChange={onChange} + isLoading={isOctopusDeployScopeValuesLoading} + options={octopusDeployScopeValues?.Channels} + placeholder={ + octopusDeployScopeValues?.Channels?.length + ? "Select channels..." + : "No channels found..." + } + isDisabled={!octopusDeployScopeValues?.Channels?.length} + /> + + )} + /> +
+ + + + ); +} + +OctopusDeployCreateIntegrationPage.requireAuth = true; diff --git a/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx b/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx index 90282e1ed..b20b15a09 100644 --- a/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx +++ b/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx @@ -140,6 +140,9 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) => case "azure-devops": link = `${window.location.origin}/integrations/azure-devops/authorize`; break; + case "octopus-deploy": + link = `${window.location.origin}/integrations/octopus-deploy/authorize`; + break; default: break; } diff --git a/frontend/src/views/IntegrationsPage/components/IntegrationsSection/ConfiguredIntegrationItem.tsx b/frontend/src/views/IntegrationsPage/components/IntegrationsSection/ConfiguredIntegrationItem.tsx index 32ce21192..2be3d1f3e 100644 --- a/frontend/src/views/IntegrationsPage/components/IntegrationsSection/ConfiguredIntegrationItem.tsx +++ b/frontend/src/views/IntegrationsPage/components/IntegrationsSection/ConfiguredIntegrationItem.tsx @@ -76,6 +76,14 @@ export const ConfiguredIntegrationItem = ({ {integrationSlugNameMapping[integration.integration]}
+ {integration.integration === "octopus-deploy" && ( +
+ +
+ {integration.targetEnvironment || integration.targetEnvironmentId} +
+
+ )} {integration.integration === "qovery" && (
@@ -108,6 +116,7 @@ export const ConfiguredIntegrationItem = ({ (integration.integration === "qovery" && integration?.scope) || (integration.integration === "circleci" && "Project") || (integration.integration === "bitbucket" && "Repository") || + (integration.integration === "octopus-deploy" && "Project") || (integration.integration === "aws-secret-manager" && "Secret") || (["aws-parameter-store", "rundeck"].includes(integration.integration) && "Path") || (integration?.integration === "terraform-cloud" && "Project") ||