diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 0f88e269c..4e8902204 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1857,6 +1857,10 @@ export const AppConnections = { WINDMILL: { instanceUrl: "The Windmill instance URL to connect with (defaults to https://app.windmill.dev).", accessToken: "The access token to use to connect with Windmill." + }, + TEAMCITY: { + instanceUrl: "The TeamCity instance URL to connect with.", + accessToken: "The access token to use to connect with TeamCity." } } }; @@ -1996,6 +2000,10 @@ export const SecretSyncs = { WINDMILL: { workspace: "The Windmill workspace to sync secrets to.", path: "The Windmill workspace path to sync secrets to." + }, + TEAMCITY: { + project: "The TeamCity project to sync secrets to.", + buildConfig: "The TeamCity build configuration to sync secrets to." } } }; diff --git a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts index 25bef7270..d472a9cb1 100644 --- a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts @@ -33,6 +33,10 @@ import { PostgresConnectionListItemSchema, SanitizedPostgresConnectionSchema } from "@app/services/app-connection/postgres"; +import { + SanitizedTeamCityConnectionSchema, + TeamCityConnectionListItemSchema +} from "@app/services/app-connection/teamcity"; import { SanitizedTerraformCloudConnectionSchema, TerraformCloudConnectionListItemSchema @@ -59,7 +63,8 @@ const SanitizedAppConnectionSchema = z.union([ ...SanitizedMsSqlConnectionSchema.options, ...SanitizedCamundaConnectionSchema.options, ...SanitizedWindmillConnectionSchema.options, - ...SanitizedAuth0ConnectionSchema.options + ...SanitizedAuth0ConnectionSchema.options, + ...SanitizedTeamCityConnectionSchema.options ]); const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ @@ -76,7 +81,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ MsSqlConnectionListItemSchema, CamundaConnectionListItemSchema, WindmillConnectionListItemSchema, - Auth0ConnectionListItemSchema + Auth0ConnectionListItemSchema, + TeamCityConnectionListItemSchema ]); export const registerAppConnectionRouter = async (server: FastifyZodProvider) => { diff --git a/backend/src/server/routes/v1/app-connection-routers/index.ts b/backend/src/server/routes/v1/app-connection-routers/index.ts index a833b6882..de9db9d85 100644 --- a/backend/src/server/routes/v1/app-connection-routers/index.ts +++ b/backend/src/server/routes/v1/app-connection-routers/index.ts @@ -11,6 +11,7 @@ import { registerGitHubConnectionRouter } from "./github-connection-router"; import { registerHumanitecConnectionRouter } from "./humanitec-connection-router"; import { registerMsSqlConnectionRouter } from "./mssql-connection-router"; import { registerPostgresConnectionRouter } from "./postgres-connection-router"; +import { registerTeamCityConnectionRouter } from "./teamcity-connection-router"; import { registerTerraformCloudConnectionRouter } from "./terraform-cloud-router"; import { registerVercelConnectionRouter } from "./vercel-connection-router"; import { registerWindmillConnectionRouter } from "./windmill-connection-router"; @@ -32,5 +33,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record { + registerAppConnectionEndpoints({ + app: AppConnection.TeamCity, + server, + sanitizedResponseSchema: SanitizedTeamCityConnectionSchema, + createSchema: CreateTeamCityConnectionSchema, + updateSchema: UpdateTeamCityConnectionSchema + }); + + // The below endpoints are not exposed and for Infisical App use + server.route({ + method: "GET", + url: `/:connectionId/projects`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + response: { + 200: z + .object({ + id: z.string(), + name: z.string(), + buildTypes: z.object({ + buildType: z + .object({ + id: z.string(), + name: z.string() + }) + .array() + }) + }) + .array() + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + console.log("HIT1"); + const { connectionId } = req.params; + const projects = await server.services.appConnection.teamcity.listProjects(connectionId, req.permission); + + console.log(projects); + + return projects; + } + }); +}; diff --git a/backend/src/server/routes/v1/secret-sync-routers/index.ts b/backend/src/server/routes/v1/secret-sync-routers/index.ts index ee407cee3..a54777727 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/index.ts +++ b/backend/src/server/routes/v1/secret-sync-routers/index.ts @@ -9,6 +9,7 @@ import { registerDatabricksSyncRouter } from "./databricks-sync-router"; import { registerGcpSyncRouter } from "./gcp-sync-router"; import { registerGitHubSyncRouter } from "./github-sync-router"; import { registerHumanitecSyncRouter } from "./humanitec-sync-router"; +import { registerTeamCitySyncRouter } from "./teamcity-sync-router"; import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router"; import { registerVercelSyncRouter } from "./vercel-sync-router"; import { registerWindmillSyncRouter } from "./windmill-sync-router"; @@ -27,5 +28,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record { diff --git a/backend/src/server/routes/v1/secret-sync-routers/teamcity-sync-router.ts b/backend/src/server/routes/v1/secret-sync-routers/teamcity-sync-router.ts new file mode 100644 index 000000000..a3091aae5 --- /dev/null +++ b/backend/src/server/routes/v1/secret-sync-routers/teamcity-sync-router.ts @@ -0,0 +1,17 @@ +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { + CreateTeamCitySyncSchema, + TeamCitySyncSchema, + UpdateTeamCitySyncSchema +} from "@app/services/secret-sync/teamcity"; + +import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints"; + +export const registerTeamCitySyncRouter = async (server: FastifyZodProvider) => + registerSyncSecretsEndpoints({ + destination: SecretSync.TeamCity, + server, + responseSchema: TeamCitySyncSchema, + createSchema: CreateTeamCitySyncSchema, + updateSchema: UpdateTeamCitySyncSchema + }); diff --git a/backend/src/services/app-connection/app-connection-enums.ts b/backend/src/services/app-connection/app-connection-enums.ts index 6b6048f2a..8852ed6bc 100644 --- a/backend/src/services/app-connection/app-connection-enums.ts +++ b/backend/src/services/app-connection/app-connection-enums.ts @@ -12,7 +12,8 @@ export enum AppConnection { MsSql = "mssql", Camunda = "camunda", Windmill = "windmill", - Auth0 = "auth0" + Auth0 = "auth0", + TeamCity = "teamcity" } export enum AWSRegion { diff --git a/backend/src/services/app-connection/app-connection-fns.ts b/backend/src/services/app-connection/app-connection-fns.ts index 7e08a92b4..076f52f99 100644 --- a/backend/src/services/app-connection/app-connection-fns.ts +++ b/backend/src/services/app-connection/app-connection-fns.ts @@ -43,6 +43,11 @@ import { } from "./humanitec"; import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql"; import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres"; +import { + getTeamCityConnectionListItem, + TeamCityConnectionMethod, + validateTeamCityConnectionCredentials +} from "./teamcity"; import { getTerraformCloudConnectionListItem, TerraformCloudConnectionMethod, @@ -71,7 +76,8 @@ export const listAppConnectionOptions = () => { getMsSqlConnectionListItem(), getCamundaConnectionListItem(), getWindmillConnectionListItem(), - getAuth0ConnectionListItem() + getAuth0ConnectionListItem(), + getTeamCityConnectionListItem() ].sort((a, b) => a.name.localeCompare(b.name)); }; @@ -135,7 +141,8 @@ export const validateAppConnectionCredentials = async ( [AppConnection.Vercel]: validateVercelConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.TerraformCloud]: validateTerraformCloudConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Auth0]: validateAuth0ConnectionCredentials as TAppConnectionCredentialsValidator, - [AppConnection.Windmill]: validateWindmillConnectionCredentials as TAppConnectionCredentialsValidator + [AppConnection.Windmill]: validateWindmillConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.TeamCity]: validateTeamCityConnectionCredentials as TAppConnectionCredentialsValidator }; return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection); @@ -167,6 +174,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) => case MsSqlConnectionMethod.UsernameAndPassword: return "Username & Password"; case WindmillConnectionMethod.AccessToken: + case TeamCityConnectionMethod.AccessToken: return "Access Token"; case Auth0ConnectionMethod.ClientCredentials: return "Client Credentials"; @@ -214,5 +222,6 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record< [AppConnection.Camunda]: platformManagedCredentialsNotSupported, [AppConnection.Vercel]: platformManagedCredentialsNotSupported, [AppConnection.Windmill]: platformManagedCredentialsNotSupported, - [AppConnection.Auth0]: platformManagedCredentialsNotSupported + [AppConnection.Auth0]: platformManagedCredentialsNotSupported, + [AppConnection.TeamCity]: platformManagedCredentialsNotSupported }; diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts index 762a9bcf2..41cef4af1 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -14,5 +14,6 @@ export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.MsSql]: "Microsoft SQL Server", [AppConnection.Camunda]: "Camunda", [AppConnection.Windmill]: "Windmill", - [AppConnection.Auth0]: "Auth0" + [AppConnection.Auth0]: "Auth0", + [AppConnection.TeamCity]: "TeamCity" }; diff --git a/backend/src/services/app-connection/app-connection-service.ts b/backend/src/services/app-connection/app-connection-service.ts index 5293761a8..5ca0c1424 100644 --- a/backend/src/services/app-connection/app-connection-service.ts +++ b/backend/src/services/app-connection/app-connection-service.ts @@ -45,6 +45,8 @@ import { ValidateHumanitecConnectionCredentialsSchema } from "./humanitec"; import { humanitecConnectionService } from "./humanitec/humanitec-connection-service"; import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql"; import { ValidatePostgresConnectionCredentialsSchema } from "./postgres"; +import { ValidateTeamCityConnectionCredentialsSchema } from "./teamcity"; +import { teamcityConnectionService } from "./teamcity/teamcity-connection-service"; import { ValidateTerraformCloudConnectionCredentialsSchema } from "./terraform-cloud"; import { terraformCloudConnectionService } from "./terraform-cloud/terraform-cloud-connection-service"; import { ValidateVercelConnectionCredentialsSchema } from "./vercel"; @@ -74,7 +76,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record>>; @@ -118,6 +125,7 @@ export type TAppConnectionInput = { id: string } & ( | TCamundaConnectionInput | TWindmillConnectionInput | TAuth0ConnectionInput + | TTeamCityConnectionInput ); export type TSqlConnectionInput = TPostgresConnectionInput | TMsSqlConnectionInput; @@ -144,7 +152,8 @@ export type TAppConnectionConfig = | TSqlConnectionConfig | TCamundaConnectionConfig | TWindmillConnectionConfig - | TAuth0ConnectionConfig; + | TAuth0ConnectionConfig + | TTeamCityConnectionConfig; export type TValidateAppConnectionCredentialsSchema = | TValidateAwsConnectionCredentialsSchema @@ -160,7 +169,8 @@ export type TValidateAppConnectionCredentialsSchema = | TValidateTerraformCloudConnectionCredentialsSchema | TValidateVercelConnectionCredentialsSchema | TValidateWindmillConnectionCredentialsSchema - | TValidateAuth0ConnectionCredentialsSchema; + | TValidateAuth0ConnectionCredentialsSchema + | TValidateTeamCityConnectionCredentialsSchema; export type TListAwsConnectionKmsKeys = { connectionId: string; diff --git a/backend/src/services/app-connection/teamcity/index.ts b/backend/src/services/app-connection/teamcity/index.ts new file mode 100644 index 000000000..89433f440 --- /dev/null +++ b/backend/src/services/app-connection/teamcity/index.ts @@ -0,0 +1,4 @@ +export * from "./teamcity-connection-enums"; +export * from "./teamcity-connection-fns"; +export * from "./teamcity-connection-schemas"; +export * from "./teamcity-connection-types"; diff --git a/backend/src/services/app-connection/teamcity/teamcity-connection-enums.ts b/backend/src/services/app-connection/teamcity/teamcity-connection-enums.ts new file mode 100644 index 000000000..7e2f93cb1 --- /dev/null +++ b/backend/src/services/app-connection/teamcity/teamcity-connection-enums.ts @@ -0,0 +1,3 @@ +export enum TeamCityConnectionMethod { + AccessToken = "access-token" +} diff --git a/backend/src/services/app-connection/teamcity/teamcity-connection-fns.ts b/backend/src/services/app-connection/teamcity/teamcity-connection-fns.ts new file mode 100644 index 000000000..3c9358e68 --- /dev/null +++ b/backend/src/services/app-connection/teamcity/teamcity-connection-fns.ts @@ -0,0 +1,64 @@ +import { AxiosError } from "axios"; + +import { request } from "@app/lib/config/request"; +import { BadRequestError } from "@app/lib/errors"; +import { removeTrailingSlash } from "@app/lib/fn"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; + +import { TeamCityConnectionMethod } from "./teamcity-connection-enums"; +import { + TTeamCityConnection, + TTeamCityConnectionConfig, + TTeamCityListProjectsResponse +} from "./teamcity-connection-types"; + +export const getTeamCityConnectionListItem = () => { + return { + name: "TeamCity" as const, + app: AppConnection.TeamCity as const, + methods: Object.values(TeamCityConnectionMethod) as [TeamCityConnectionMethod.AccessToken] + }; +}; + +export const validateTeamCityConnectionCredentials = async (config: TTeamCityConnectionConfig) => { + const instanceUrl = removeTrailingSlash(config.credentials.instanceUrl); + const { accessToken } = config.credentials; + + try { + await request.get(`${instanceUrl}/app/rest/server`, { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } + }); + } catch (error: unknown) { + if (error instanceof AxiosError) { + throw new BadRequestError({ + message: `Failed to validate credentials: ${error.message || "Unknown error"}` + }); + } + throw new BadRequestError({ + message: "Unable to validate connection: verify credentials" + }); + } + + return config.credentials; +}; + +export const listTeamCityProjects = async (appConnection: TTeamCityConnection) => { + const instanceUrl = removeTrailingSlash(appConnection.credentials.instanceUrl); + const { accessToken } = appConnection.credentials; + + const resp = await request.get( + `${instanceUrl}/app/rest/projects?fields=project(id,name,buildTypes(buildType(id,name)))`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } + } + ); + + // Filter out the root project. Should not be seen by users. + return resp.data.project.filter((proj) => proj.id !== "_Root"); +}; diff --git a/backend/src/services/app-connection/teamcity/teamcity-connection-schemas.ts b/backend/src/services/app-connection/teamcity/teamcity-connection-schemas.ts new file mode 100644 index 000000000..30494e2ba --- /dev/null +++ b/backend/src/services/app-connection/teamcity/teamcity-connection-schemas.ts @@ -0,0 +1,70 @@ +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 { TeamCityConnectionMethod } from "./teamcity-connection-enums"; + +export const TeamCityConnectionAccessTokenCredentialsSchema = z.object({ + accessToken: z + .string() + .trim() + .min(1, "Access Token required") + .describe(AppConnections.CREDENTIALS.TEAMCITY.accessToken), + instanceUrl: z + .string() + .trim() + .url("Invalid Instance URL") + .min(1, "Instance URL required") + .describe(AppConnections.CREDENTIALS.TEAMCITY.instanceUrl) +}); + +const BaseTeamCityConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.TeamCity) }); + +export const TeamCityConnectionSchema = BaseTeamCityConnectionSchema.extend({ + method: z.literal(TeamCityConnectionMethod.AccessToken), + credentials: TeamCityConnectionAccessTokenCredentialsSchema +}); + +export const SanitizedTeamCityConnectionSchema = z.discriminatedUnion("method", [ + BaseTeamCityConnectionSchema.extend({ + method: z.literal(TeamCityConnectionMethod.AccessToken), + credentials: TeamCityConnectionAccessTokenCredentialsSchema.pick({ + instanceUrl: true + }) + }) +]); + +export const ValidateTeamCityConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: z + .literal(TeamCityConnectionMethod.AccessToken) + .describe(AppConnections.CREATE(AppConnection.TeamCity).method), + credentials: TeamCityConnectionAccessTokenCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.TeamCity).credentials + ) + }) +]); + +export const CreateTeamCityConnectionSchema = ValidateTeamCityConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.TeamCity) +); + +export const UpdateTeamCityConnectionSchema = z + .object({ + credentials: TeamCityConnectionAccessTokenCredentialsSchema.optional().describe( + AppConnections.UPDATE(AppConnection.TeamCity).credentials + ) + }) + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.TeamCity)); + +export const TeamCityConnectionListItemSchema = z.object({ + name: z.literal("TeamCity"), + app: z.literal(AppConnection.TeamCity), + methods: z.nativeEnum(TeamCityConnectionMethod).array() +}); diff --git a/backend/src/services/app-connection/teamcity/teamcity-connection-service.ts b/backend/src/services/app-connection/teamcity/teamcity-connection-service.ts new file mode 100644 index 000000000..afad7f572 --- /dev/null +++ b/backend/src/services/app-connection/teamcity/teamcity-connection-service.ts @@ -0,0 +1,28 @@ +import { OrgServiceActor } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { listTeamCityProjects } from "./teamcity-connection-fns"; +import { TTeamCityConnection } from "./teamcity-connection-types"; + +type TGetAppConnectionFunc = ( + app: AppConnection, + connectionId: string, + actor: OrgServiceActor +) => Promise; + +export const teamcityConnectionService = (getAppConnection: TGetAppConnectionFunc) => { + const listProjects = async (connectionId: string, actor: OrgServiceActor) => { + const appConnection = await getAppConnection(AppConnection.TeamCity, connectionId, actor); + + try { + const projects = await listTeamCityProjects(appConnection); + return projects; + } catch (error) { + return []; + } + }; + + return { + listProjects + }; +}; diff --git a/backend/src/services/app-connection/teamcity/teamcity-connection-types.ts b/backend/src/services/app-connection/teamcity/teamcity-connection-types.ts new file mode 100644 index 000000000..737e7c70d --- /dev/null +++ b/backend/src/services/app-connection/teamcity/teamcity-connection-types.ts @@ -0,0 +1,43 @@ +import z from "zod"; + +import { DiscriminativePick } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { + CreateTeamCityConnectionSchema, + TeamCityConnectionSchema, + ValidateTeamCityConnectionCredentialsSchema +} from "./teamcity-connection-schemas"; + +export type TTeamCityConnection = z.infer; + +export type TTeamCityConnectionInput = z.infer & { + app: AppConnection.TeamCity; +}; + +export type TValidateTeamCityConnectionCredentialsSchema = typeof ValidateTeamCityConnectionCredentialsSchema; + +export type TTeamCityConnectionConfig = DiscriminativePick< + TTeamCityConnectionInput, + "method" | "app" | "credentials" +> & { + orgId: string; +}; + +export type TTeamCityProject = { + id: string; + name: string; +}; + +export type TTeamCityProjectWithBuildTypes = TTeamCityProject & { + buildTypes: { + buildType: { + id: string; + name: string; + }[]; + }; +}; + +export type TTeamCityListProjectsResponse = { + project: TTeamCityProjectWithBuildTypes[]; +}; diff --git a/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts b/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts index 0849a7523..fe7b24783 100644 --- a/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts +++ b/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts @@ -68,18 +68,15 @@ const awsRegionFromHeader = (authorizationHeader: string): string | null => { return null; }; - - -function isValidAwsRegion(region: (string | null)): boolean { - const validRegionPattern = new RE2('^[a-z0-9-]+$'); - if (typeof region !== 'string' || region.length === 0 || region.length > 20) { +function isValidAwsRegion(region: string | null): boolean { + const validRegionPattern = new RE2("^[a-z0-9-]+$"); + if (typeof region !== "string" || region.length === 0 || region.length > 20) { return false; } - + return validRegionPattern.test(region); } - export const identityAwsAuthServiceFactory = ({ identityAccessTokenDAL, identityAwsAuthDAL, @@ -100,7 +97,7 @@ export const identityAwsAuthServiceFactory = ({ const region = headers.Authorization ? awsRegionFromHeader(headers.Authorization) : null; if (!isValidAwsRegion(region)) { - throw new BadRequestError({message: "Invalid AWS region"}); + throw new BadRequestError({ message: "Invalid AWS region" }); } const url = region ? `https://sts.${region}.amazonaws.com` : identityAwsAuth.stsEndpoint; diff --git a/backend/src/services/secret-sync/secret-sync-enums.ts b/backend/src/services/secret-sync/secret-sync-enums.ts index 86273a4ff..687d76f33 100644 --- a/backend/src/services/secret-sync/secret-sync-enums.ts +++ b/backend/src/services/secret-sync/secret-sync-enums.ts @@ -10,7 +10,8 @@ export enum SecretSync { TerraformCloud = "terraform-cloud", Camunda = "camunda", Vercel = "vercel", - Windmill = "windmill" + Windmill = "windmill", + TeamCity = "teamcity" } export enum SecretSyncInitialSyncBehavior { diff --git a/backend/src/services/secret-sync/secret-sync-fns.ts b/backend/src/services/secret-sync/secret-sync-fns.ts index 0b821b593..8e9554d00 100644 --- a/backend/src/services/secret-sync/secret-sync-fns.ts +++ b/backend/src/services/secret-sync/secret-sync-fns.ts @@ -27,6 +27,7 @@ import { GCP_SYNC_LIST_OPTION } from "./gcp"; import { GcpSyncFns } from "./gcp/gcp-sync-fns"; import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec"; import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns"; +import { TEAMCITY_SYNC_LIST_OPTION, TeamCitySyncFns } from "./teamcity"; import { TERRAFORM_CLOUD_SYNC_LIST_OPTION, TerraformCloudSyncFns } from "./terraform-cloud"; import { VERCEL_SYNC_LIST_OPTION, VercelSyncFns } from "./vercel"; import { WINDMILL_SYNC_LIST_OPTION, WindmillSyncFns } from "./windmill"; @@ -43,7 +44,8 @@ const SECRET_SYNC_LIST_OPTIONS: Record = { [SecretSync.TerraformCloud]: TERRAFORM_CLOUD_SYNC_LIST_OPTION, [SecretSync.Camunda]: CAMUNDA_SYNC_LIST_OPTION, [SecretSync.Vercel]: VERCEL_SYNC_LIST_OPTION, - [SecretSync.Windmill]: WINDMILL_SYNC_LIST_OPTION + [SecretSync.Windmill]: WINDMILL_SYNC_LIST_OPTION, + [SecretSync.TeamCity]: TEAMCITY_SYNC_LIST_OPTION }; export const listSecretSyncOptions = () => { @@ -140,10 +142,10 @@ export const SecretSyncFns = { return VercelSyncFns.syncSecrets(secretSync, secretMap); case SecretSync.Windmill: return WindmillSyncFns.syncSecrets(secretSync, secretMap); + case SecretSync.TeamCity: + return TeamCitySyncFns.syncSecrets(secretSync, secretMap); default: - throw new Error( - `Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` - ); + throw new Error(`Unhandled sync destination for sync secrets fns: ${secretSync.destination}`); } }, getSecrets: async ( @@ -199,10 +201,11 @@ export const SecretSyncFns = { case SecretSync.Windmill: secretMap = await WindmillSyncFns.getSecrets(secretSync); break; + case SecretSync.TeamCity: + secretMap = await TeamCitySyncFns.getSecrets(secretSync); + break; default: - throw new Error( - `Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` - ); + throw new Error(`Unhandled sync destination for get secrets fns: ${secretSync.destination}`); } return secretMap; @@ -252,10 +255,10 @@ export const SecretSyncFns = { return VercelSyncFns.removeSecrets(secretSync, secretMap); case SecretSync.Windmill: return WindmillSyncFns.removeSecrets(secretSync, secretMap); + case SecretSync.TeamCity: + return TeamCitySyncFns.removeSecrets(secretSync, secretMap); default: - throw new Error( - `Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` - ); + throw new Error(`Unhandled sync destination for remove secrets fns: ${secretSync.destination}`); } } }; diff --git a/backend/src/services/secret-sync/secret-sync-maps.ts b/backend/src/services/secret-sync/secret-sync-maps.ts index a9099543d..fdf4dfabb 100644 --- a/backend/src/services/secret-sync/secret-sync-maps.ts +++ b/backend/src/services/secret-sync/secret-sync-maps.ts @@ -13,7 +13,8 @@ export const SECRET_SYNC_NAME_MAP: Record = { [SecretSync.TerraformCloud]: "Terraform Cloud", [SecretSync.Camunda]: "Camunda", [SecretSync.Vercel]: "Vercel", - [SecretSync.Windmill]: "Windmill" + [SecretSync.Windmill]: "Windmill", + [SecretSync.TeamCity]: "TeamCity" }; export const SECRET_SYNC_CONNECTION_MAP: Record = { @@ -28,5 +29,6 @@ export const SECRET_SYNC_CONNECTION_MAP: Record = { [SecretSync.TerraformCloud]: AppConnection.TerraformCloud, [SecretSync.Camunda]: AppConnection.Camunda, [SecretSync.Vercel]: AppConnection.Vercel, - [SecretSync.Windmill]: AppConnection.Windmill + [SecretSync.Windmill]: AppConnection.Windmill, + [SecretSync.TeamCity]: AppConnection.TeamCity }; diff --git a/backend/src/services/secret-sync/secret-sync-types.ts b/backend/src/services/secret-sync/secret-sync-types.ts index 03e92a57a..716c9b44f 100644 --- a/backend/src/services/secret-sync/secret-sync-types.ts +++ b/backend/src/services/secret-sync/secret-sync-types.ts @@ -61,6 +61,12 @@ import { THumanitecSyncListItem, THumanitecSyncWithCredentials } from "./humanitec"; +import { + TTeamCitySync, + TTeamCitySyncInput, + TTeamCitySyncListItem, + TTeamCitySyncWithCredentials +} from "./teamcity/teamcity-sync-types"; import { TTerraformCloudSync, TTerraformCloudSyncInput, @@ -81,7 +87,8 @@ export type TSecretSync = | TTerraformCloudSync | TCamundaSync | TVercelSync - | TWindmillSync; + | TWindmillSync + | TTeamCitySync; export type TSecretSyncWithCredentials = | TAwsParameterStoreSyncWithCredentials @@ -95,7 +102,8 @@ export type TSecretSyncWithCredentials = | TTerraformCloudSyncWithCredentials | TCamundaSyncWithCredentials | TVercelSyncWithCredentials - | TWindmillSyncWithCredentials; + | TWindmillSyncWithCredentials + | TTeamCitySyncWithCredentials; export type TSecretSyncInput = | TAwsParameterStoreSyncInput @@ -109,7 +117,8 @@ export type TSecretSyncInput = | TTerraformCloudSyncInput | TCamundaSyncInput | TVercelSyncInput - | TWindmillSyncInput; + | TWindmillSyncInput + | TTeamCitySyncInput; export type TSecretSyncListItem = | TAwsParameterStoreSyncListItem @@ -123,7 +132,8 @@ export type TSecretSyncListItem = | TTerraformCloudSyncListItem | TCamundaSyncListItem | TVercelSyncListItem - | TWindmillSyncListItem; + | TWindmillSyncListItem + | TTeamCitySyncListItem; export type TSyncOptionsConfig = { canImportSecrets: boolean; diff --git a/backend/src/services/secret-sync/teamcity/index.ts b/backend/src/services/secret-sync/teamcity/index.ts new file mode 100644 index 000000000..add83cb20 --- /dev/null +++ b/backend/src/services/secret-sync/teamcity/index.ts @@ -0,0 +1,4 @@ +export * from "./teamcity-sync-constants"; +export * from "./teamcity-sync-fns"; +export * from "./teamcity-sync-schemas"; +export * from "./teamcity-sync-types"; diff --git a/backend/src/services/secret-sync/teamcity/teamcity-sync-constants.ts b/backend/src/services/secret-sync/teamcity/teamcity-sync-constants.ts new file mode 100644 index 000000000..30f541bbc --- /dev/null +++ b/backend/src/services/secret-sync/teamcity/teamcity-sync-constants.ts @@ -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 TEAMCITY_SYNC_LIST_OPTION: TSecretSyncListItem = { + name: "TeamCity", + destination: SecretSync.TeamCity, + connection: AppConnection.TeamCity, + canImportSecrets: true +}; diff --git a/backend/src/services/secret-sync/teamcity/teamcity-sync-fns.ts b/backend/src/services/secret-sync/teamcity/teamcity-sync-fns.ts new file mode 100644 index 000000000..685351ef5 --- /dev/null +++ b/backend/src/services/secret-sync/teamcity/teamcity-sync-fns.ts @@ -0,0 +1,182 @@ +import { request } from "@app/lib/config/request"; +import { removeTrailingSlash } from "@app/lib/fn"; +import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; +import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; +import { + TDeleteTeamCityVariable, + TPostTeamCityVariable, + TTeamCityListVariables, + TTeamCityListVariablesResponse, + TTeamCitySyncWithCredentials +} from "@app/services/secret-sync/teamcity/teamcity-sync-types"; + +// Note: Most variables won't be returned with a value due to them being a "password" type (starting with "env."). +const listTeamCityVariables = async ({ instanceUrl, accessToken, project, buildConfig }: TTeamCityListVariables) => { + const { data } = await request.get( + buildConfig + ? `${instanceUrl}/app/rest/buildTypes/${buildConfig}/parameters` + : `${instanceUrl}/app/rest/projects/id:${project}/parameters`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } + } + ); + + // Strips out "env." from map key, but the "name" field still has the original unaltered key. + return Object.fromEntries( + data.property.map((variable) => [ + variable.name.startsWith("env.") ? variable.name.substring(4) : variable.name, + { ...variable, value: variable.value || "" } // This will almost always be empty string + ]) + ); +}; + +// Create and update both use the same method +const updateTeamCityVariable = async ({ + instanceUrl, + accessToken, + project, + buildConfig, + key, + value +}: TPostTeamCityVariable) => { + return request.post( + buildConfig + ? `${instanceUrl}/app/rest/buildTypes/${buildConfig}/parameters` + : `${instanceUrl}/app/rest/projects/id:${project}/parameters`, + { + name: key, + value, + type: { + rawValue: "password display='hidden'" + } + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json" + } + } + ); +}; + +const deleteTeamCityVariable = async ({ + instanceUrl, + accessToken, + project, + buildConfig, + key +}: TDeleteTeamCityVariable) => { + return request.delete( + buildConfig + ? `${instanceUrl}/app/rest/buildTypes/${buildConfig}/parameters/${key}` + : `${instanceUrl}/app/rest/projects/${project}/parameters/${key}`, + { + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ); +}; + +export const TeamCitySyncFns = { + syncSecrets: async (secretSync: TTeamCitySyncWithCredentials, secretMap: TSecretMap) => { + const { + connection, + destinationConfig: { project, buildConfig } + } = secretSync; + + const instanceUrl = removeTrailingSlash(connection.credentials.instanceUrl); + const { accessToken } = connection.credentials; + + for await (const entry of Object.entries(secretMap)) { + const [key, { value }] = entry; + + const payload = { + instanceUrl, + accessToken, + project, + buildConfig, + key: `env.${key}`, + value + }; + + try { + // Replace every secret since TeamCity does not return secret values that we can cross-check + // No need to differenciate create / update because TeamCity uses the same method for both + await updateTeamCityVariable(payload); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); + } + } + + if (secretSync.syncOptions.disableSecretDeletion) return; + + const variables = await listTeamCityVariables({ instanceUrl, accessToken, project, buildConfig }); + + for await (const [key, variable] of Object.entries(variables)) { + if (!(key in secretMap)) { + try { + await deleteTeamCityVariable({ + key: variable.name, // We use variable.name instead of key because key is stripped of "env." prefix in listTeamCityVariables(). + instanceUrl, + accessToken, + project, + buildConfig + }); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); + } + } + } + }, + removeSecrets: async (secretSync: TTeamCitySyncWithCredentials, secretMap: TSecretMap) => { + const { + connection, + destinationConfig: { project, buildConfig } + } = secretSync; + + const instanceUrl = removeTrailingSlash(connection.credentials.instanceUrl); + const { accessToken } = connection.credentials; + + const variables = await listTeamCityVariables({ instanceUrl, accessToken, project, buildConfig }); + + for await (const [key, variable] of Object.entries(variables)) { + if (key in secretMap) { + try { + await deleteTeamCityVariable({ + key: variable.name, // We use variable.name instead of key because key is stripped of "env." prefix in listTeamCityVariables(). + instanceUrl, + accessToken, + project, + buildConfig + }); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: variable.name + }); + } + } + } + }, + getSecrets: async (secretSync: TTeamCitySyncWithCredentials) => { + const { + connection, + destinationConfig: { project, buildConfig } + } = secretSync; + + const instanceUrl = removeTrailingSlash(connection.credentials.instanceUrl); + const { accessToken } = connection.credentials; + + return listTeamCityVariables({ instanceUrl, accessToken, project, buildConfig }); + } +}; diff --git a/backend/src/services/secret-sync/teamcity/teamcity-sync-schemas.ts b/backend/src/services/secret-sync/teamcity/teamcity-sync-schemas.ts new file mode 100644 index 000000000..21c09092f --- /dev/null +++ b/backend/src/services/secret-sync/teamcity/teamcity-sync-schemas.ts @@ -0,0 +1,44 @@ +import { z } from "zod"; + +import { SecretSyncs } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { + BaseSecretSyncSchema, + GenericCreateSecretSyncFieldsSchema, + GenericUpdateSecretSyncFieldsSchema +} from "@app/services/secret-sync/secret-sync-schemas"; +import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; + +const TeamCitySyncDestinationConfigSchema = z.object({ + project: z.string().trim().min(1, "Project required").describe(SecretSyncs.DESTINATION_CONFIG.TEAMCITY.project), + buildConfig: z.string().trim().optional().describe(SecretSyncs.DESTINATION_CONFIG.TEAMCITY.buildConfig) +}); + +const TeamCitySyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true }; + +export const TeamCitySyncSchema = BaseSecretSyncSchema(SecretSync.TeamCity, TeamCitySyncOptionsConfig).extend({ + destination: z.literal(SecretSync.TeamCity), + destinationConfig: TeamCitySyncDestinationConfigSchema +}); + +export const CreateTeamCitySyncSchema = GenericCreateSecretSyncFieldsSchema( + SecretSync.TeamCity, + TeamCitySyncOptionsConfig +).extend({ + destinationConfig: TeamCitySyncDestinationConfigSchema +}); + +export const UpdateTeamCitySyncSchema = GenericUpdateSecretSyncFieldsSchema( + SecretSync.TeamCity, + TeamCitySyncOptionsConfig +).extend({ + destinationConfig: TeamCitySyncDestinationConfigSchema.optional() +}); + +export const TeamCitySyncListItemSchema = z.object({ + name: z.literal("TeamCity"), + connection: z.literal(AppConnection.TeamCity), + destination: z.literal(SecretSync.TeamCity), + canImportSecrets: z.literal(true) +}); diff --git a/backend/src/services/secret-sync/teamcity/teamcity-sync-types.ts b/backend/src/services/secret-sync/teamcity/teamcity-sync-types.ts new file mode 100644 index 000000000..8b3f15e0d --- /dev/null +++ b/backend/src/services/secret-sync/teamcity/teamcity-sync-types.ts @@ -0,0 +1,46 @@ +import { z } from "zod"; + +import { TTeamCityConnection } from "@app/services/app-connection/teamcity"; + +import { CreateTeamCitySyncSchema, TeamCitySyncListItemSchema, TeamCitySyncSchema } from "./teamcity-sync-schemas"; + +export type TTeamCitySync = z.infer; + +export type TTeamCitySyncInput = z.infer; + +export type TTeamCitySyncListItem = z.infer; + +export type TTeamCitySyncWithCredentials = TTeamCitySync & { + connection: TTeamCityConnection; +}; + +export type TTeamCityVariable = { + name: string; + value: string; + inherited?: boolean; + type: { + rawValue: string; + }; +}; + +export type TTeamCityListVariablesResponse = { + property: (TTeamCityVariable & { value?: string })[]; + count: number; + href: string; +}; + +export type TTeamCityListVariables = { + accessToken: string; + instanceUrl: string; + project: string; + buildConfig?: string; +}; + +export type TPostTeamCityVariable = TTeamCityListVariables & { + key: string; + value: string; +}; + +export type TDeleteTeamCityVariable = TTeamCityListVariables & { + key: string; +}; diff --git a/docs/api-reference/endpoints/app-connections/teamcity/available.mdx b/docs/api-reference/endpoints/app-connections/teamcity/available.mdx new file mode 100644 index 000000000..c5cbd3c9d --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/teamcity/available.mdx @@ -0,0 +1,4 @@ +--- +title: "Available" +openapi: "GET /api/v1/app-connections/teamcity/available" +--- diff --git a/docs/api-reference/endpoints/app-connections/teamcity/create.mdx b/docs/api-reference/endpoints/app-connections/teamcity/create.mdx new file mode 100644 index 000000000..19d30af70 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/teamcity/create.mdx @@ -0,0 +1,9 @@ +--- +title: "Create" +openapi: "POST /api/v1/app-connections/teamcity" +--- + + + Check out the configuration docs for [TeamCity Connections](/integrations/app-connections/teamcity) to learn how to obtain + the required credentials. + diff --git a/docs/api-reference/endpoints/app-connections/teamcity/delete.mdx b/docs/api-reference/endpoints/app-connections/teamcity/delete.mdx new file mode 100644 index 000000000..d4a5d67ae --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/teamcity/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/teamcity/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/teamcity/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/teamcity/get-by-id.mdx new file mode 100644 index 000000000..090725826 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/teamcity/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/teamcity/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/teamcity/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/teamcity/get-by-name.mdx new file mode 100644 index 000000000..ccb46a27d --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/teamcity/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/teamcity/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/teamcity/list.mdx b/docs/api-reference/endpoints/app-connections/teamcity/list.mdx new file mode 100644 index 000000000..e4987a876 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/teamcity/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/teamcity" +--- diff --git a/docs/api-reference/endpoints/app-connections/teamcity/update.mdx b/docs/api-reference/endpoints/app-connections/teamcity/update.mdx new file mode 100644 index 000000000..499f4a379 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/teamcity/update.mdx @@ -0,0 +1,9 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/teamcity/{connectionId}" +--- + + + Check out the configuration docs for [TeamCity Connections](/integrations/app-connections/teamcity) to learn how to obtain + the required credentials. + diff --git a/docs/api-reference/endpoints/secret-syncs/teamcity/create.mdx b/docs/api-reference/endpoints/secret-syncs/teamcity/create.mdx new file mode 100644 index 000000000..438702b34 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/teamcity/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-syncs/teamcity" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/teamcity/delete.mdx b/docs/api-reference/endpoints/secret-syncs/teamcity/delete.mdx new file mode 100644 index 000000000..f2b1af6eb --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/teamcity/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-syncs/teamcity/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/teamcity/get-by-id.mdx b/docs/api-reference/endpoints/secret-syncs/teamcity/get-by-id.mdx new file mode 100644 index 000000000..857d1d5a2 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/teamcity/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/secret-syncs/teamcity/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/teamcity/get-by-name.mdx b/docs/api-reference/endpoints/secret-syncs/teamcity/get-by-name.mdx new file mode 100644 index 000000000..e7101c7b7 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/teamcity/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/secret-syncs/teamcity/sync-name/{syncName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/teamcity/import-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/teamcity/import-secrets.mdx new file mode 100644 index 000000000..961259300 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/teamcity/import-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Import Secrets" +openapi: "POST /api/v1/secret-syncs/teamcity/{syncId}/import-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/teamcity/list.mdx b/docs/api-reference/endpoints/secret-syncs/teamcity/list.mdx new file mode 100644 index 000000000..d3a6a8373 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/teamcity/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-syncs/teamcity" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/teamcity/remove-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/teamcity/remove-secrets.mdx new file mode 100644 index 000000000..0f63752ba --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/teamcity/remove-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Secrets" +openapi: "POST /api/v1/secret-syncs/teamcity/{syncId}/remove-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/teamcity/sync-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/teamcity/sync-secrets.mdx new file mode 100644 index 000000000..36eaa361c --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/teamcity/sync-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Secrets" +openapi: "POST /api/v1/secret-syncs/teamcity/{syncId}/sync-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/teamcity/update.mdx b/docs/api-reference/endpoints/secret-syncs/teamcity/update.mdx new file mode 100644 index 000000000..820c81b21 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/teamcity/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/secret-syncs/teamcity/{syncId}" +--- diff --git a/docs/images/app-connections/general/add-connection.png b/docs/images/app-connections/general/add-connection.png index 97718065a..ad9d54716 100644 Binary files a/docs/images/app-connections/general/add-connection.png and b/docs/images/app-connections/general/add-connection.png differ diff --git a/docs/images/app-connections/teamcity/teamcity-app-connection-created.png b/docs/images/app-connections/teamcity/teamcity-app-connection-created.png new file mode 100644 index 000000000..698894139 Binary files /dev/null and b/docs/images/app-connections/teamcity/teamcity-app-connection-created.png differ diff --git a/docs/images/app-connections/teamcity/teamcity-app-connection-modal.png b/docs/images/app-connections/teamcity/teamcity-app-connection-modal.png new file mode 100644 index 000000000..9e602dc1b Binary files /dev/null and b/docs/images/app-connections/teamcity/teamcity-app-connection-modal.png differ diff --git a/docs/images/app-connections/teamcity/teamcity-app-connection-option.png b/docs/images/app-connections/teamcity/teamcity-app-connection-option.png new file mode 100644 index 000000000..52d001126 Binary files /dev/null and b/docs/images/app-connections/teamcity/teamcity-app-connection-option.png differ diff --git a/docs/images/app-connections/teamcity/teamcity-main-page.png b/docs/images/app-connections/teamcity/teamcity-main-page.png new file mode 100644 index 000000000..4ee08d774 Binary files /dev/null and b/docs/images/app-connections/teamcity/teamcity-main-page.png differ diff --git a/docs/images/app-connections/teamcity/teamcity-token-copy.png b/docs/images/app-connections/teamcity/teamcity-token-copy.png new file mode 100644 index 000000000..1aa363104 Binary files /dev/null and b/docs/images/app-connections/teamcity/teamcity-token-copy.png differ diff --git a/docs/images/app-connections/teamcity/teamcity-token-created.png b/docs/images/app-connections/teamcity/teamcity-token-created.png new file mode 100644 index 000000000..c909f4107 Binary files /dev/null and b/docs/images/app-connections/teamcity/teamcity-token-created.png differ diff --git a/docs/images/app-connections/teamcity/teamcity-token-page.png b/docs/images/app-connections/teamcity/teamcity-token-page.png new file mode 100644 index 000000000..1730a99ef Binary files /dev/null and b/docs/images/app-connections/teamcity/teamcity-token-page.png differ diff --git a/docs/images/app-connections/teamcity/teamcity-token-popup.png b/docs/images/app-connections/teamcity/teamcity-token-popup.png new file mode 100644 index 000000000..0e18d37ea Binary files /dev/null and b/docs/images/app-connections/teamcity/teamcity-token-popup.png differ diff --git a/docs/images/secret-syncs/general/secret-sync-tab.png b/docs/images/secret-syncs/general/secret-sync-tab.png index dad8c2426..5317fabf0 100644 Binary files a/docs/images/secret-syncs/general/secret-sync-tab.png and b/docs/images/secret-syncs/general/secret-sync-tab.png differ diff --git a/docs/images/secret-syncs/teamcity/select-teamcity-option.png b/docs/images/secret-syncs/teamcity/select-teamcity-option.png new file mode 100644 index 000000000..261f53163 Binary files /dev/null and b/docs/images/secret-syncs/teamcity/select-teamcity-option.png differ diff --git a/docs/images/secret-syncs/teamcity/teamcity-sync-created.png b/docs/images/secret-syncs/teamcity/teamcity-sync-created.png new file mode 100644 index 000000000..871b94db9 Binary files /dev/null and b/docs/images/secret-syncs/teamcity/teamcity-sync-created.png differ diff --git a/docs/images/secret-syncs/teamcity/teamcity-sync-destination.png b/docs/images/secret-syncs/teamcity/teamcity-sync-destination.png new file mode 100644 index 000000000..bc546fc3b Binary files /dev/null and b/docs/images/secret-syncs/teamcity/teamcity-sync-destination.png differ diff --git a/docs/images/secret-syncs/teamcity/teamcity-sync-details.png b/docs/images/secret-syncs/teamcity/teamcity-sync-details.png new file mode 100644 index 000000000..02b890926 Binary files /dev/null and b/docs/images/secret-syncs/teamcity/teamcity-sync-details.png differ diff --git a/docs/images/secret-syncs/teamcity/teamcity-sync-options.png b/docs/images/secret-syncs/teamcity/teamcity-sync-options.png new file mode 100644 index 000000000..d9ef23fb0 Binary files /dev/null and b/docs/images/secret-syncs/teamcity/teamcity-sync-options.png differ diff --git a/docs/images/secret-syncs/teamcity/teamcity-sync-review.png b/docs/images/secret-syncs/teamcity/teamcity-sync-review.png new file mode 100644 index 000000000..753b7ba17 Binary files /dev/null and b/docs/images/secret-syncs/teamcity/teamcity-sync-review.png differ diff --git a/docs/images/secret-syncs/teamcity/teamcity-sync-source.png b/docs/images/secret-syncs/teamcity/teamcity-sync-source.png new file mode 100644 index 000000000..dc69e1db8 Binary files /dev/null and b/docs/images/secret-syncs/teamcity/teamcity-sync-source.png differ diff --git a/docs/integrations/app-connections/teamcity.mdx b/docs/integrations/app-connections/teamcity.mdx new file mode 100644 index 000000000..9fc6bff89 --- /dev/null +++ b/docs/integrations/app-connections/teamcity.mdx @@ -0,0 +1,108 @@ +--- +title: "TeamCity Connection" +description: "Learn how to configure a TeamCity Connection for Infisical." +--- + +Infisical supports connecting to TeamCity using an Access Token to securely sync your secrets to TeamCity. + +## Setup TeamCity Connection in Infisical + + + + Navigate to the TeamCity **Profile** page by clicking on your profile icon in the bottom-left corner. + ![TeamCity Main Page](/images/app-connections/teamcity/teamcity-main-page.png) + + + Select the **Access Tokens** tab from the left sidebar navigation menu. + ![TeamCity Token Page](/images/app-connections/teamcity/teamcity-token-page.png) + + + Click the **Create access token** button and provide a name for your token (e.g., "Infisical Integration"). You may set an expiration date or leave it blank for no expiry. + The permission scope can either be **Same as current user** or **Limit per project**. If you're choosing **Limit per project**, make sure you select the relevant project and enable the **View build configuration settings** and **Edit project** permissions. + + Setting your permission scope to **Same as current user** will allow your integration to access multiple projects as long as the current user has read and write access to them. + + + Setting your permission scope to **Limit per project** requires the **View build configuration settings** and **Edit project** permissions. + + ![TeamCity Token Popup](/images/app-connections/teamcity/teamcity-token-popup.png) + + + After creation, a modal with the API token will be displayed. Copy this token immediately and store it securely, as you won't be able to view it again after closing this dialog. + ![TeamCity Token Copy Popup](/images/app-connections/teamcity/teamcity-token-copy.png) + + + You should now see your newly created token in the list of access tokens. + ![TeamCity Token Created](/images/app-connections/teamcity/teamcity-token-created.png) + + + + + 1. Navigate to App Connections + + In your Infisical dashboard, go to **Organization Settings** and select the [**App Connections**](https://app.infisical.com/organization/app-connections) tab. + ![App Connections Tab](/images/app-connections/general/add-connection.png) + 2. Add Connection + + Click the **+ Add Connection** button and select the **TeamCity Connection** option from the available integrations. + ![Select TeamCity Connection](/images/app-connections/teamcity/teamcity-app-connection-option.png) + 3. Fill the TeamCity Connection Modal + + Complete the TeamCity Connection form by entering: + - A descriptive name for the connection + - The Access Token you generated in steps 3-4 + - The URL of your TeamCity instance + - An optional description for future reference + + ![TeamCity Connection Modal](/images/app-connections/teamcity/teamcity-app-connection-modal.png) + 4. Connection Created + + After clicking Create, your **TeamCity Connection** is established and ready to use with your Infisical projects. + ![TeamCity Connection Created](/images/app-connections/teamcity/teamcity-app-connection-created.png) + + + To create a TeamCity Connection, make an API request to the [Create TeamCity + Connection](/api-reference/endpoints/app-connections/teamcity/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/app-connections/teamcity \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-teamcity-connection", + "method": "access-token", + "credentials": { + "accessToken": "...", + "instanceUrl": "https://yourcompany.teamcity.com" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "appConnection": { + "id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6", + "name": "my-teamcity-connection", + "description": null, + "version": 1, + "orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c", + "createdAt": "2025-04-23T19:46:34.831Z", + "updatedAt": "2025-04-23T19:46:34.831Z", + "isPlatformManagedCredentials": false, + "credentialsHash": "7c2d371dec195f82a6a0d5b41c970a229cfcaf88e894a5b6395e2dbd0280661f", + "app": "teamcity", + "method": "access-token", + "credentials": { + "instanceUrl": "https://yourcompany.teamcity.com" + } + } + } + ``` + + + + diff --git a/docs/integrations/secret-syncs/teamcity.mdx b/docs/integrations/secret-syncs/teamcity.mdx new file mode 100644 index 000000000..e79fc0f0c --- /dev/null +++ b/docs/integrations/secret-syncs/teamcity.mdx @@ -0,0 +1,147 @@ +--- +title: "TeamCity Sync" +description: "Learn how to configure a TeamCity Sync for Infisical." +--- + +**Prerequisites:** + + - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) + - Create a [TeamCity Connection](/integrations/app-connections/teamcity) with the required **Secret Sync** permissions + + + + 1. Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button. + ![Secret Syncs Tab](/images/secret-syncs/general/secret-sync-tab.png) + + 2. Select the **TeamCity** option. + ![Select TeamCity](/images/secret-syncs/teamcity/select-teamcity-option.png) + + 3. Configure the **Source** from where secrets should be retrieved, then click **Next**. + ![Configure Source](/images/secret-syncs/teamcity/teamcity-sync-source.png) + + - **Environment**: The project environment to retrieve secrets from. + - **Secret Path**: The folder path to retrieve secrets from. + + + If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports). + + + 4. Configure the **Destination** to where secrets should be deployed, then click **Next**. + ![Configure Destination](/images/secret-syncs/teamcity/teamcity-sync-destination.png) + + - **TeamCity Connection**: The TeamCity Connection to authenticate with. + - **Project**: The TeamCity project to sync secrets to. + - **Build Configuration**: The build configuration to sync secrets to. + + + Not including a Build Configuration will sync secrets to the entire project. + + + 5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. + ![Configure Options](/images/secret-syncs/teamcity/teamcity-sync-options.png) + + - **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync. + - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. + - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over TeamCity when keys conflict. + - **Import Secrets (Prioritize TeamCity)**: Imports secrets from the destination endpoint before syncing, prioritizing values from TeamCity over Infisical when keys conflict. + - **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. + + 6. Configure the **Details** of your TeamCity Sync, then click **Next**. + ![Configure Details](/images/secret-syncs/teamcity/teamcity-sync-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + 7. Review your TeamCity Sync configuration, then click **Create Sync**. + ![Confirm Configuration](/images/secret-syncs/teamcity/teamcity-sync-review.png) + + 8. If enabled, your TeamCity Sync will begin syncing your secrets to the destination endpoint. + ![Sync Secrets](/images/secret-syncs/teamcity/teamcity-sync-created.png) + + + + To create a **TeamCity Sync**, make an API request to the [Create TeamCity Sync](/api-reference/endpoints/secret-syncs/teamcity/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/secret-syncs/teamcity \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-teamcity-sync", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "an example sync", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "environment": "dev", + "secretPath": "/my-secrets", + "isEnabled": true, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "destinationConfig": { + "project": "TestProject", + "buildConfig": "TestBuildConfig" + } + }' + ``` + + + The **Project** and **Build Config** parameters must use project and build configuration IDs, not their names. + + + ### Sample response + + ```bash Response + { + "secretSync": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-teamcity-sync", + "description": "an example sync", + "isEnabled": true, + "version": 1, + "folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "syncStatus": "succeeded", + "lastSyncJobId": "123", + "lastSyncMessage": null, + "lastSyncedAt": "2023-11-07T05:31:56Z", + "importStatus": null, + "lastImportJobId": null, + "lastImportMessage": null, + "lastImportedAt": null, + "removeStatus": null, + "lastRemoveJobId": null, + "lastRemoveMessage": null, + "lastRemovedAt": null, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connection": { + "app": "teamcity", + "name": "my-teamcity-connection", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "environment": { + "slug": "dev", + "name": "Development", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "folder": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "path": "/my-secrets" + }, + "destination": "teamcity", + "destinationConfig": { + "project": "TestProject", + "buildConfig": "TestBuildConfig" + } + } + } + ``` + + diff --git a/docs/mint.json b/docs/mint.json index ee07fb048..e9c94f9f2 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -428,7 +428,8 @@ "integrations/app-connections/postgres", "integrations/app-connections/terraform-cloud", "integrations/app-connections/vercel", - "integrations/app-connections/windmill" + "integrations/app-connections/windmill", + "integrations/app-connections/teamcity" ] } ] @@ -451,7 +452,8 @@ "integrations/secret-syncs/humanitec", "integrations/secret-syncs/terraform-cloud", "integrations/secret-syncs/vercel", - "integrations/secret-syncs/windmill" + "integrations/secret-syncs/windmill", + "integrations/secret-syncs/teamcity" ] } ] @@ -558,9 +560,7 @@ }, { "group": "Others", - "pages": [ - "integrations/external/backstage" - ] + "pages": ["integrations/external/backstage"] }, { "group": "", @@ -1072,6 +1072,18 @@ "api-reference/endpoints/app-connections/windmill/update", "api-reference/endpoints/app-connections/windmill/delete" ] + }, + { + "group": "TeamCity", + "pages": [ + "api-reference/endpoints/app-connections/teamcity/list", + "api-reference/endpoints/app-connections/teamcity/available", + "api-reference/endpoints/app-connections/teamcity/get-by-id", + "api-reference/endpoints/app-connections/teamcity/get-by-name", + "api-reference/endpoints/app-connections/teamcity/create", + "api-reference/endpoints/app-connections/teamcity/update", + "api-reference/endpoints/app-connections/teamcity/delete" + ] } ] }, @@ -1242,6 +1254,20 @@ "api-reference/endpoints/secret-syncs/windmill/import-secrets", "api-reference/endpoints/secret-syncs/windmill/remove-secrets" ] + }, + { + "group": "TeamCity", + "pages": [ + "api-reference/endpoints/secret-syncs/teamcity/list", + "api-reference/endpoints/secret-syncs/teamcity/get-by-id", + "api-reference/endpoints/secret-syncs/teamcity/get-by-name", + "api-reference/endpoints/secret-syncs/teamcity/create", + "api-reference/endpoints/secret-syncs/teamcity/update", + "api-reference/endpoints/secret-syncs/teamcity/delete", + "api-reference/endpoints/secret-syncs/teamcity/sync-secrets", + "api-reference/endpoints/secret-syncs/teamcity/import-secrets", + "api-reference/endpoints/secret-syncs/teamcity/remove-secrets" + ] } ] }, diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx index b2008b4f6..47afc6f04 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx @@ -12,6 +12,7 @@ import { DatabricksSyncFields } from "./DatabricksSyncFields"; import { GcpSyncFields } from "./GcpSyncFields"; import { GitHubSyncFields } from "./GitHubSyncFields"; import { HumanitecSyncFields } from "./HumanitecSyncFields"; +import { TeamCitySyncFields } from "./TeamCitySyncFields"; import { TerraformCloudSyncFields } from "./TerraformCloudSyncFields"; import { VercelSyncFields } from "./VercelSyncFields"; import { WindmillSyncFields } from "./WindmillSyncFields"; @@ -46,6 +47,8 @@ export const SecretSyncDestinationFields = () => { return ; case SecretSync.Windmill: return ; + case SecretSync.TeamCity: + return ; default: throw new Error(`Unhandled Destination Config Field: ${destination}`); } diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/TeamCitySyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/TeamCitySyncFields.tsx new file mode 100644 index 000000000..23b5b7501 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/TeamCitySyncFields.tsx @@ -0,0 +1,117 @@ +import { Controller, useFormContext, useWatch } from "react-hook-form"; +import { 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, Tooltip } from "@app/components/v2"; +import { + TTeamCityProjectWithBuildTypes, + useTeamCityConnectionListProjects +} from "@app/hooks/api/appConnections/teamcity"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +import { TSecretSyncForm } from "../schemas"; + +export const TeamCitySyncFields = () => { + const { control, setValue } = useFormContext< + TSecretSyncForm & { destination: SecretSync.TeamCity } + >(); + + const connectionId = useWatch({ name: "connection.id", control }); + + const { data: projects, isLoading: isProjectsLoading } = useTeamCityConnectionListProjects( + connectionId, + { + enabled: Boolean(connectionId) + } + ); + + // For Build Config dropdown + const selectedProjectId = useWatch({ name: "destinationConfig.project", control }); + const selectedProject = projects?.find((proj) => proj.id === selectedProjectId); + + const buildTypes = selectedProject?.buildTypes?.buildType || []; + + return ( + <> + { + setValue("destinationConfig.project", ""); + setValue("destinationConfig.buildConfig", ""); + }} + /> + + ( + +
+ Don't see the project you're looking for?{" "} + +
+ + } + > + proj.id === value) ?? null} + onChange={(option) => { + onChange((option as SingleValue)?.id ?? null); + setValue("destinationConfig.buildConfig", ""); + }} + options={projects} + placeholder="Select a project..." + getOptionLabel={(option) => option.name} + getOptionValue={(option) => option.id} + /> +
+ )} + /> + + ( + + Not selecting a Build Configuration will sync your secrets with the entire project. + + } + > + buildType.id === value) ?? null} + onChange={(option) => { + const selectedOption = option as SingleValue<{ id: string; name: string }>; + onChange(selectedOption?.id ?? ""); + }} + options={buildTypes} + placeholder="Select a build configuration..." + getOptionLabel={(option) => option.name} + getOptionValue={(option) => option.id} + /> + + )} + /> + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx index 79c437d27..e2c285b6f 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx @@ -43,6 +43,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => { case SecretSync.Camunda: case SecretSync.Vercel: case SecretSync.Windmill: + case SecretSync.TeamCity: AdditionalSyncOptionsFieldsComponent = null; break; default: diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx index 99182f207..da9651535 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx @@ -22,6 +22,7 @@ import { DatabricksSyncReviewFields } from "./DatabricksSyncReviewFields"; import { GcpSyncReviewFields } from "./GcpSyncReviewFields"; import { GitHubSyncReviewFields } from "./GitHubSyncReviewFields"; import { HumanitecSyncReviewFields } from "./HumanitecSyncReviewFields"; +import { TeamCitySyncReviewFields } from "./TeamCitySyncReviewFields"; import { TerraformCloudSyncReviewFields } from "./TerraformCloudSyncReviewFields"; import { VercelSyncReviewFields } from "./VercelSyncReviewFields"; import { WindmillSyncReviewFields } from "./WindmillSyncReviewFields"; @@ -88,6 +89,9 @@ export const SecretSyncReviewFields = () => { case SecretSync.Windmill: DestinationFieldsComponent = ; break; + case SecretSync.TeamCity: + DestinationFieldsComponent = ; + break; default: throw new Error(`Unhandled Destination Review Fields: ${destination}`); } diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/TeamCitySyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/TeamCitySyncReviewFields.tsx new file mode 100644 index 000000000..277ebe1b4 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/TeamCitySyncReviewFields.tsx @@ -0,0 +1,18 @@ +import { useFormContext } from "react-hook-form"; + +import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas"; +import { GenericFieldLabel } from "@app/components/v2"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +export const TeamCitySyncReviewFields = () => { + const { watch } = useFormContext(); + const project = watch("destinationConfig.project"); + const buildConfig = watch("destinationConfig.buildConfig"); + + return ( + <> + {project} + {buildConfig} + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts index d58e60b19..50221dc39 100644 --- a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts +++ b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts @@ -9,6 +9,7 @@ import { DatabricksSyncDestinationSchema } from "./databricks-sync-destination-s import { GcpSyncDestinationSchema } from "./gcp-sync-destination-schema"; import { GitHubSyncDestinationSchema } from "./github-sync-destination-schema"; import { HumanitecSyncDestinationSchema } from "./humanitec-sync-destination-schema"; +import { TeamCitySyncDestinationSchema } from "./teamcity-sync-destination-schema"; import { TerraformCloudSyncDestinationSchema } from "./terraform-cloud-destination-schema"; import { VercelSyncDestinationSchema } from "./vercel-sync-destination-schema"; import { WindmillSyncDestinationSchema } from "./windmill-sync-destination-schema"; @@ -25,7 +26,8 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [ TerraformCloudSyncDestinationSchema, CamundaSyncDestinationSchema, VercelSyncDestinationSchema, - WindmillSyncDestinationSchema + WindmillSyncDestinationSchema, + TeamCitySyncDestinationSchema ]); export const SecretSyncFormSchema = SecretSyncUnionSchema; diff --git a/frontend/src/components/secret-syncs/forms/schemas/teamcity-sync-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/teamcity-sync-destination-schema.ts new file mode 100644 index 000000000..e2cd60050 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/schemas/teamcity-sync-destination-schema.ts @@ -0,0 +1,14 @@ +import { z } from "zod"; + +import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +export const TeamCitySyncDestinationSchema = BaseSecretSyncSchema().merge( + z.object({ + destination: z.literal(SecretSync.TeamCity), + destinationConfig: z.object({ + project: z.string().trim().min(1, "Project required"), + buildConfig: z.string().trim().optional() + }) + }) +); diff --git a/frontend/src/helpers/appConnections.ts b/frontend/src/helpers/appConnections.ts index c94bd6648..fd8b6666d 100644 --- a/frontend/src/helpers/appConnections.ts +++ b/frontend/src/helpers/appConnections.ts @@ -15,6 +15,7 @@ import { MsSqlConnectionMethod, PostgresConnectionMethod, TAppConnection, + TeamCityConnectionMethod, TerraformCloudConnectionMethod, VercelConnectionMethod, WindmillConnectionMethod @@ -43,7 +44,8 @@ export const APP_CONNECTION_MAP: Record< [AppConnection.MsSql]: { name: "Microsoft SQL Server", image: "MsSql.png" }, [AppConnection.Camunda]: { name: "Camunda", image: "Camunda.png" }, [AppConnection.Windmill]: { name: "Windmill", image: "Windmill.png" }, - [AppConnection.Auth0]: { name: "Auth0", image: "Auth0.png", size: 40 } + [AppConnection.Auth0]: { name: "Auth0", image: "Auth0.png", size: 40 }, + [AppConnection.TeamCity]: { name: "TeamCity", image: "TeamCity.png" } }; export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => { @@ -71,6 +73,7 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) case PostgresConnectionMethod.UsernameAndPassword: case MsSqlConnectionMethod.UsernameAndPassword: return { name: "Username & Password", icon: faLock }; + case TeamCityConnectionMethod.AccessToken: case WindmillConnectionMethod.AccessToken: return { name: "Access Token", icon: faKey }; case Auth0ConnectionMethod.ClientCredentials: diff --git a/frontend/src/helpers/secretSyncs.ts b/frontend/src/helpers/secretSyncs.ts index 291b6f80a..009c2804b 100644 --- a/frontend/src/helpers/secretSyncs.ts +++ b/frontend/src/helpers/secretSyncs.ts @@ -39,6 +39,10 @@ export const SECRET_SYNC_MAP: Record = { [SecretSync.TerraformCloud]: AppConnection.TerraformCloud, [SecretSync.Camunda]: AppConnection.Camunda, [SecretSync.Vercel]: AppConnection.Vercel, - [SecretSync.Windmill]: AppConnection.Windmill + [SecretSync.Windmill]: AppConnection.Windmill, + [SecretSync.TeamCity]: AppConnection.TeamCity }; export const SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP: Record< diff --git a/frontend/src/hooks/api/appConnections/enums.ts b/frontend/src/hooks/api/appConnections/enums.ts index 704b200bb..31e2df71f 100644 --- a/frontend/src/hooks/api/appConnections/enums.ts +++ b/frontend/src/hooks/api/appConnections/enums.ts @@ -12,5 +12,6 @@ export enum AppConnection { MsSql = "mssql", Camunda = "camunda", Windmill = "windmill", - Auth0 = "auth0" + Auth0 = "auth0", + TeamCity = "teamcity" } diff --git a/frontend/src/hooks/api/appConnections/teamcity/index.ts b/frontend/src/hooks/api/appConnections/teamcity/index.ts new file mode 100644 index 000000000..2c1906d36 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/teamcity/index.ts @@ -0,0 +1,2 @@ +export * from "./queries"; +export * from "./types"; diff --git a/frontend/src/hooks/api/appConnections/teamcity/queries.tsx b/frontend/src/hooks/api/appConnections/teamcity/queries.tsx new file mode 100644 index 000000000..9d117c265 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/teamcity/queries.tsx @@ -0,0 +1,37 @@ +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { appConnectionKeys } from "../queries"; +import { TTeamCityProjectWithBuildTypes } from "./types"; + +const teamcityConnectionKeys = { + all: [...appConnectionKeys.all, "teamcity"] as const, + listProjects: (connectionId: string) => + [...teamcityConnectionKeys.all, "projects", connectionId] as const +}; + +export const useTeamCityConnectionListProjects = ( + connectionId: string, + options?: Omit< + UseQueryOptions< + TTeamCityProjectWithBuildTypes[], + unknown, + TTeamCityProjectWithBuildTypes[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: teamcityConnectionKeys.listProjects(connectionId), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/app-connections/teamcity/${connectionId}/projects` + ); + + return data; + }, + ...options + }); +}; diff --git a/frontend/src/hooks/api/appConnections/teamcity/types.ts b/frontend/src/hooks/api/appConnections/teamcity/types.ts new file mode 100644 index 000000000..a7adab034 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/teamcity/types.ts @@ -0,0 +1,13 @@ +export type TTeamCityProject = { + id: string; + name: string; +}; + +export type TTeamCityProjectWithBuildTypes = TTeamCityProject & { + buildTypes: { + buildType: { + id: string; + name: string; + }[]; + }; +}; diff --git a/frontend/src/hooks/api/appConnections/types/app-options.ts b/frontend/src/hooks/api/appConnections/types/app-options.ts index ae57fbda2..3122ca32c 100644 --- a/frontend/src/hooks/api/appConnections/types/app-options.ts +++ b/frontend/src/hooks/api/appConnections/types/app-options.ts @@ -67,6 +67,10 @@ export type TAuth0ConnectionOption = TAppConnectionOptionBase & { app: AppConnection.Auth0; }; +export type TTeamCityConnectionOption = TAppConnectionOptionBase & { + app: AppConnection.TeamCity; +}; + export type TAppConnectionOption = | TAwsConnectionOption | TGitHubConnectionOption @@ -81,7 +85,8 @@ export type TAppConnectionOption = | TMsSqlConnectionOption | TCamundaConnectionOption | TWindmillConnectionOption - | TAuth0ConnectionOption; + | TAuth0ConnectionOption + | TTeamCityConnectionOption; export type TAppConnectionOptionMap = { [AppConnection.AWS]: TAwsConnectionOption; @@ -98,4 +103,5 @@ export type TAppConnectionOptionMap = { [AppConnection.Camunda]: TCamundaConnectionOption; [AppConnection.Windmill]: TWindmillConnectionOption; [AppConnection.Auth0]: TAuth0ConnectionOption; + [AppConnection.TeamCity]: TTeamCityConnectionOption; }; diff --git a/frontend/src/hooks/api/appConnections/types/index.ts b/frontend/src/hooks/api/appConnections/types/index.ts index 29e82bff6..929ca796d 100644 --- a/frontend/src/hooks/api/appConnections/types/index.ts +++ b/frontend/src/hooks/api/appConnections/types/index.ts @@ -11,6 +11,7 @@ import { TGitHubConnection } from "./github-connection"; import { THumanitecConnection } from "./humanitec-connection"; import { TMsSqlConnection } from "./mssql-connection"; import { TPostgresConnection } from "./postgres-connection"; +import { TTeamCityConnection } from "./teamcity-connection"; import { TTerraformCloudConnection } from "./terraform-cloud-connection"; import { TVercelConnection } from "./vercel-connection"; import { TWindmillConnection } from "./windmill-connection"; @@ -26,6 +27,7 @@ export * from "./github-connection"; export * from "./humanitec-connection"; export * from "./mssql-connection"; export * from "./postgres-connection"; +export * from "./teamcity-connection"; export * from "./terraform-cloud-connection"; export * from "./vercel-connection"; export * from "./windmill-connection"; @@ -44,7 +46,8 @@ export type TAppConnection = | TMsSqlConnection | TCamundaConnection | TWindmillConnection - | TAuth0Connection; + | TAuth0Connection + | TTeamCityConnection; export type TAvailableAppConnection = Pick; @@ -86,4 +89,5 @@ export type TAppConnectionMap = { [AppConnection.Camunda]: TCamundaConnection; [AppConnection.Windmill]: TWindmillConnection; [AppConnection.Auth0]: TAuth0Connection; + [AppConnection.TeamCity]: TTeamCityConnection; }; diff --git a/frontend/src/hooks/api/appConnections/types/teamcity-connection.ts b/frontend/src/hooks/api/appConnections/types/teamcity-connection.ts new file mode 100644 index 000000000..69a32b670 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/types/teamcity-connection.ts @@ -0,0 +1,14 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection"; + +export enum TeamCityConnectionMethod { + AccessToken = "access-token" +} + +export type TTeamCityConnection = TRootAppConnection & { app: AppConnection.TeamCity } & { + method: TeamCityConnectionMethod.AccessToken; + credentials: { + accessToken: string; + instanceUrl?: string; + }; +}; diff --git a/frontend/src/hooks/api/secretSyncs/enums.ts b/frontend/src/hooks/api/secretSyncs/enums.ts index b0d3fd7bb..450df773a 100644 --- a/frontend/src/hooks/api/secretSyncs/enums.ts +++ b/frontend/src/hooks/api/secretSyncs/enums.ts @@ -10,7 +10,8 @@ export enum SecretSync { TerraformCloud = "terraform-cloud", Camunda = "camunda", Vercel = "vercel", - Windmill = "windmill" + Windmill = "windmill", + TeamCity = "teamcity" } export enum SecretSyncStatus { diff --git a/frontend/src/hooks/api/secretSyncs/types/index.ts b/frontend/src/hooks/api/secretSyncs/types/index.ts index 21fda56c7..e9ac538fe 100644 --- a/frontend/src/hooks/api/secretSyncs/types/index.ts +++ b/frontend/src/hooks/api/secretSyncs/types/index.ts @@ -10,6 +10,7 @@ import { TDatabricksSync } from "./databricks-sync"; import { TGcpSync } from "./gcp-sync"; import { TGitHubSync } from "./github-sync"; import { THumanitecSync } from "./humanitec-sync"; +import { TTeamCitySync } from "./teamcity-sync"; import { TTerraformCloudSync } from "./terraform-cloud-sync"; import { TVercelSync } from "./vercel-sync"; import { TWindmillSync } from "./windmill-sync"; @@ -32,7 +33,8 @@ export type TSecretSync = | TTerraformCloudSync | TCamundaSync | TVercelSync - | TWindmillSync; + | TWindmillSync + | TTeamCitySync; export type TListSecretSyncs = { secretSyncs: TSecretSync[] }; diff --git a/frontend/src/hooks/api/secretSyncs/types/teamcity-sync.ts b/frontend/src/hooks/api/secretSyncs/types/teamcity-sync.ts new file mode 100644 index 000000000..17f218d06 --- /dev/null +++ b/frontend/src/hooks/api/secretSyncs/types/teamcity-sync.ts @@ -0,0 +1,16 @@ +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 type TTeamCitySync = TRootSecretSync & { + destination: SecretSync.TeamCity; + destinationConfig: { + project: string; + buildConfig?: string; + }; + connection: { + app: AppConnection.TeamCity; + name: string; + id: string; + }; +}; diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx index 05a7eebbc..1259c50d6 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx @@ -20,6 +20,7 @@ import { GitHubConnectionForm } from "./GitHubConnectionForm"; import { HumanitecConnectionForm } from "./HumanitecConnectionForm"; import { MsSqlConnectionForm } from "./MsSqlConnectionForm"; import { PostgresConnectionForm } from "./PostgresConnectionForm"; +import { TeamCityConnectionForm } from "./TeamCityConnectionForm"; import { TerraformCloudConnectionForm } from "./TerraformCloudConnectionForm"; import { VercelConnectionForm } from "./VercelConnectionForm"; import { WindmillConnectionForm } from "./WindmillConnectionForm"; @@ -89,6 +90,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => { return ; case AppConnection.Auth0: return ; + case AppConnection.TeamCity: + return ; default: throw new Error(`Unhandled App ${app}`); } @@ -153,6 +156,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => { return ; case AppConnection.Auth0: return ; + case AppConnection.TeamCity: + return ; default: throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`); } diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/TeamCityConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/TeamCityConnectionForm.tsx new file mode 100644 index 000000000..ef0421a86 --- /dev/null +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/TeamCityConnectionForm.tsx @@ -0,0 +1,157 @@ +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 { TeamCityConnectionMethod, TTeamCityConnection } from "@app/hooks/api/appConnections"; +import { AppConnection } from "@app/hooks/api/appConnections/enums"; + +import { + genericAppConnectionFieldsSchema, + GenericAppConnectionsFields +} from "./GenericAppConnectionFields"; + +type Props = { + appConnection?: TTeamCityConnection; + onSubmit: (formData: FormData) => void; +}; + +const rootSchema = genericAppConnectionFieldsSchema.extend({ + app: z.literal(AppConnection.TeamCity) +}); + +const formSchema = z.discriminatedUnion("method", [ + rootSchema.extend({ + method: z.literal(TeamCityConnectionMethod.AccessToken), + credentials: z.object({ + accessToken: z.string().trim().min(1, "Access Token required"), + instanceUrl: z + .string() + .trim() + .transform((value) => value || undefined) + .refine((value) => (!value ? true : z.string().url().safeParse(value).success), { + message: "Invalid instance URL" + }) + .optional() + }) + }) +]); + +type FormData = z.infer; + +export const TeamCityConnectionForm = ({ appConnection, onSubmit }: Props) => { + const isUpdate = Boolean(appConnection); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: appConnection ?? { + app: AppConnection.TeamCity, + method: TeamCityConnectionMethod.AccessToken + } + }); + + const { + handleSubmit, + control, + formState: { isSubmitting, isDirty } + } = form; + + return ( + +
+ {!isUpdate && } + ( + + + + )} + /> + ( + + + + )} + /> + ( + + onChange(e.target.value)} + /> + + )} + /> +
+ + + + +
+ +
+ ); +}; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx index 03371c8ce..776f4665e 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx @@ -9,6 +9,7 @@ import { DatabricksSyncDestinationCol } from "./DatabricksSyncDestinationCol"; import { GcpSyncDestinationCol } from "./GcpSyncDestinationCol"; import { GitHubSyncDestinationCol } from "./GitHubSyncDestinationCol"; import { HumanitecSyncDestinationCol } from "./HumanitecSyncDestinationCol"; +import { TeamCitySyncDestinationCol } from "./TeamCitySyncDestinationCol"; import { TerraformCloudSyncDestinationCol } from "./TerraformCloudSyncDestinationCol"; import { VercelSyncDestinationCol } from "./VercelSyncDestinationCol"; import { WindmillSyncDestinationCol } from "./WindmillSyncDestinationCol"; @@ -43,6 +44,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => { return ; case SecretSync.Windmill: return ; + case SecretSync.TeamCity: + return ; default: throw new Error( `Unhandled Secret Sync Destination Col: ${(secretSync as TSecretSync).destination}` diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/TeamCitySyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/TeamCitySyncDestinationCol.tsx new file mode 100644 index 000000000..b26304aa5 --- /dev/null +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/TeamCitySyncDestinationCol.tsx @@ -0,0 +1,14 @@ +import { TTeamCitySync } from "@app/hooks/api/secretSyncs/types/teamcity-sync"; + +import { getSecretSyncDestinationColValues } from "../helpers"; +import { SecretSyncTableCell } from "../SecretSyncTableCell"; + +type Props = { + secretSync: TTeamCitySync; +}; + +export const TeamCitySyncDestinationCol = ({ secretSync }: Props) => { + const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync); + + return ; +}; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts index 7ef5644ed..27dc9d5a1 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts @@ -94,6 +94,10 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => { primaryText = destinationConfig.workspace; secondaryText = destinationConfig.path; break; + case SecretSync.TeamCity: + primaryText = destinationConfig.project; + secondaryText = destinationConfig.buildConfig; + break; default: throw new Error(`Unhandled Destination Col Values ${destination}`); } diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx index a3c948b94..505f3ac3b 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx @@ -19,6 +19,7 @@ import { DatabricksSyncDestinationSection } from "./DatabricksSyncDestinationSec import { GcpSyncDestinationSection } from "./GcpSyncDestinationSection"; import { GitHubSyncDestinationSection } from "./GitHubSyncDestinationSection"; import { HumanitecSyncDestinationSection } from "./HumanitecSyncDestinationSection"; +import { TeamCitySyncDestinationSection } from "./TeamCitySyncDestinationSection"; import { TerraformCloudSyncDestinationSection } from "./TerraformCloudSyncDestinationSection"; import { VercelSyncDestinationSection } from "./VercelSyncDestinationSection"; import { WindmillSyncDestinationSection } from "./WindmillSyncDestinationSection"; @@ -73,6 +74,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }: case SecretSync.Windmill: DestinationComponents = ; break; + case SecretSync.TeamCity: + DestinationComponents = ; + break; default: throw new Error(`Unhandled Destination Section components: ${destination}`); } diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/TeamCitySyncDestinationSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/TeamCitySyncDestinationSection.tsx new file mode 100644 index 000000000..f712eaa20 --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/TeamCitySyncDestinationSection.tsx @@ -0,0 +1,19 @@ +import { GenericFieldLabel } from "@app/components/secret-syncs"; +import { TTeamCitySync } from "@app/hooks/api/secretSyncs/types/teamcity-sync"; + +type Props = { + secretSync: TTeamCitySync; +}; + +export const TeamCitySyncDestinationSection = ({ secretSync }: Props) => { + const { + destinationConfig: { project, buildConfig } + } = secretSync; + + return ( + <> + {project} + {buildConfig} + + ); +}; diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx index 4fe701c8a..bc6713d3d 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx @@ -52,6 +52,7 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) = case SecretSync.Camunda: case SecretSync.Vercel: case SecretSync.Windmill: + case SecretSync.TeamCity: AdditionalSyncOptionsComponent = null; break; default: