Finish adding support for build-configuration level syncs for TeamCity integration

This commit is contained in:
Tuan Dang
2023-08-29 14:37:58 +01:00
parent dc0d577cbb
commit a6e9643464
23 changed files with 314 additions and 90 deletions

View File

@@ -547,6 +547,57 @@ export const getIntegrationAuthNorthflankSecretGroups = async (req: Request, res
});
}
/**
* Return list of build configs for TeamCity project with id [appId]
* @param req
* @param res
* @returns
*/
export const getIntegrationAuthTeamCityBuildConfigs = async (req: Request, res: Response) => {
const appId = req.query.appId as string;
interface TeamCityBuildConfig {
id: string;
name: string;
projectName: string;
projectId: string;
href: string;
webUrl: string;
}
interface GetTeamCityBuildConfigsRes {
count: number;
href: string;
buildType: TeamCityBuildConfig[];
}
if (appId && appId !== "") {
const { data: { buildType } } = (
await standardRequest.get<GetTeamCityBuildConfigsRes>(`${req.integrationAuth.url}/app/rest/buildTypes`, {
params: {
locator: `project:${appId}`
},
headers: {
Authorization: `Bearer ${req.accessToken}`,
Accept: "application/json",
},
})
);
return res.status(200).send({
buildConfigs: buildType.map((buildConfig) => ({
name: buildConfig.name,
buildConfigId: buildConfig.id
}))
});
}
return res.status(200).send({
buildConfigs: []
});
}
/**
* Delete integration authorization with id [integrationAuthId]
* @param req

View File

@@ -236,7 +236,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
version: 1
},
$unset: {
'metadata.source': true as true
"metadata.source": true as const
},
...u,
_id: new Types.ObjectId(u._id)

View File

@@ -9,7 +9,6 @@ import {
INTEGRATION_VERCEL
} from "../variables";
import { UnauthorizedRequestError } from "../utils/errors";
import { syncSecretsToActiveIntegrationsQueue } from "../queues/integrations/syncSecretsToThirdPartyServices"
interface Update {
workspace: string;

View File

@@ -850,7 +850,7 @@ const getAppsTeamCity = async ({
},
})
).data.project.slice(1);
const apps = res.map((a: any) => {
return {
name: a.name,

View File

@@ -4,6 +4,8 @@ import {
INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_BITBUCKET,
INTEGRATION_BITBUCKET_TOKEN_URL,
INTEGRATION_GCP_SECRET_MANAGER,
INTEGRATION_GCP_TOKEN_URL,
INTEGRATION_GITHUB,
INTEGRATION_GITHUB_TOKEN_URL,
INTEGRATION_GITLAB,
@@ -13,21 +15,19 @@ import {
INTEGRATION_NETLIFY,
INTEGRATION_NETLIFY_TOKEN_URL,
INTEGRATION_VERCEL,
INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_GCP_SECRET_MANAGER,
INTEGRATION_GCP_TOKEN_URL
INTEGRATION_VERCEL_TOKEN_URL
} from "../variables";
import {
getClientIdGCPSecretManager,
getClientSecretGCPSecretManager,
getClientIdAzure,
getClientIdBitBucket,
getClientIdGCPSecretManager,
getClientIdGitHub,
getClientIdGitLab,
getClientIdNetlify,
getClientIdVercel,
getClientSecretAzure,
getClientSecretBitBucket,
getClientSecretGCPSecretManager,
getClientSecretGitHub,
getClientSecretGitLab,
getClientSecretHeroku,

View File

@@ -2185,7 +2185,7 @@ const syncSecretsTerraformCloud = async ({
};
/**
* Sync/push [secrets] to TeamCity project
* Sync/push [secrets] to TeamCity project (and optionally build config)
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {Object} obj.secrets - secrets to push to integration
@@ -2207,57 +2207,124 @@ const syncSecretsTeamCity = async ({
value: string;
}
// get secrets from Teamcity
const res = (
await standardRequest.get(
`${integrationAuth.url}/app/rest/projects/id:${integration.appId}/parameters`,
interface TeamCityBuildConfigParameter {
name: string;
value: string;
inherited: boolean;
}
interface GetTeamCityBuildConfigParametersRes {
href: string;
count: number;
property: TeamCityBuildConfigParameter[];
}
if (integration.targetEnvironment && integration.targetEnvironmentId) {
// case: sync to specific build-config in TeamCity project
const res = (await standardRequest.get<GetTeamCityBuildConfigParametersRes>(
`${integrationAuth.url}/app/rest/buildTypes/${integration.targetEnvironmentId}/parameters`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json"
}
Accept: "application/json",
},
}
)
).data.property.reduce((obj: any, secret: TeamCitySecret) => {
const secretName = secret.name.replace(/^env\./, "");
return {
...obj,
[secretName]: secret.value
};
}, {});
for await (const key of Object.keys(secrets)) {
if (!(key in res) || (key in res && secrets[key] !== res[key])) {
// case: secret does not exist in TeamCity or secret value has changed
// -> create/update secret
await standardRequest.post(
`${integrationAuth.url}/app/rest/projects/id:${integration.appId}/parameters`,
))
.data
.property
.filter((parameter) => !parameter.inherited)
.reduce((obj: any, secret: TeamCitySecret) => {
const secretName = secret.name.replace(/^env\./, "");
return {
...obj,
[secretName]: secret.value
};
}, {});
for await (const key of Object.keys(secrets)) {
if (!(key in res) || (key in res && secrets[key].value !== res[key])) {
// case: secret does not exist in TeamCity or secret value has changed
// -> create/update secret
await standardRequest.post(`${integrationAuth.url}/app/rest/buildTypes/${integration.targetEnvironmentId}/parameters`,
{
name: `env.${key}`,
name:`env.${key}`,
value: secrets[key].value
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json"
}
}
);
Accept: "application/json",
},
});
}
}
}
for await (const key of Object.keys(res)) {
if (!(key in secrets)) {
// delete secret
await standardRequest.delete(
`${integrationAuth.url}/app/rest/projects/id:${integration.appId}/parameters/env.${key}`,
for await (const key of Object.keys(res)) {
if (!(key in secrets)) {
// delete secret
await standardRequest.delete(
`${integrationAuth.url}/app/rest/buildTypes/${integration.targetEnvironmentId}/parameters/env.${key}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json"
}
}
);
}
}
} else {
// case: sync to TeamCity project
const res = (
await standardRequest.get(
`${integrationAuth.url}/app/rest/projects/id:${integration.appId}/parameters`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json"
}
}
);
)
).data.property.reduce((obj: any, secret: TeamCitySecret) => {
const secretName = secret.name.replace(/^env\./, "");
return {
...obj,
[secretName]: secret.value
};
}, {});
for await (const key of Object.keys(secrets)) {
if (!(key in res) || (key in res && secrets[key] !== res[key])) {
// case: secret does not exist in TeamCity or secret value has changed
// -> create/update secret
await standardRequest.post(
`${integrationAuth.url}/app/rest/projects/id:${integration.appId}/parameters`,
{
name: `env.${key}`,
value: secrets[key].value
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json"
}
}
);
}
}
for await (const key of Object.keys(res)) {
if (!(key in secrets)) {
// delete secret
await standardRequest.delete(
`${integrationAuth.url}/app/rest/projects/id:${integration.appId}/parameters/env.${key}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json"
}
}
);
}
}
}
};

View File

@@ -10,6 +10,7 @@ import {
INTEGRATION_CODEFRESH,
INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM,
INTEGRATION_FLYIO,
INTEGRATION_GCP_SECRET_MANAGER,
INTEGRATION_GITHUB,
INTEGRATION_GITLAB,
INTEGRATION_HASHICORP_VAULT,
@@ -24,8 +25,7 @@ import {
INTEGRATION_TERRAFORM_CLOUD,
INTEGRATION_TRAVISCI,
INTEGRATION_VERCEL,
INTEGRATION_WINDMILL,
INTEGRATION_GCP_SECRET_MANAGER
INTEGRATION_WINDMILL
} from "../variables";
import { Schema, Types, model } from "mongoose";

View File

@@ -12,6 +12,7 @@ import {
INTEGRATION_CODEFRESH,
INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM,
INTEGRATION_FLYIO,
INTEGRATION_GCP_SECRET_MANAGER,
INTEGRATION_GITHUB,
INTEGRATION_GITLAB,
INTEGRATION_HASHICORP_VAULT,
@@ -26,8 +27,7 @@ import {
INTEGRATION_TERRAFORM_CLOUD,
INTEGRATION_TRAVISCI,
INTEGRATION_VERCEL,
INTEGRATION_WINDMILL,
INTEGRATION_GCP_SECRET_MANAGER
INTEGRATION_WINDMILL
} from "../variables";
import { Document, Schema, Types, model } from "mongoose";

View File

@@ -1,16 +1,16 @@
import Queue, { Job } from "bull";
import { ProbotOctokit } from "probot"
import { Commit, Committer, Repository } from "@octokit/webhooks-types";
import { Commit } from "@octokit/webhooks-types";
import TelemetryService from "../../services/TelemetryService";
import { sendMail } from "../../helpers";
import GitRisks from "../../ee/models/gitRisks";
import { MembershipOrg, User } from "../../models";
import { OWNER, ADMIN } from "../../variables";
import { ADMIN, OWNER } from "../../variables";
import { convertKeysToLowercase, scanContentAndGetFindings } from "../../ee/services/GithubSecretScanning/helper";
import { getSecretScanningGitAppId, getSecretScanningPrivateKey } from "../../config";
import { SecretMatch } from "../../ee/services/GithubSecretScanning/types";
export const githubPushEventSecretScan = new Queue('github-push-event-secret-scanning', 'redis://redis:6379');
export const githubPushEventSecretScan = new Queue("github-push-event-secret-scanning", "redis://redis:6379");
type TScanPushEventQueueDetails = {
organizationId: string,

View File

@@ -89,4 +89,4 @@ router.post(
integrationController.manualSync
);
export default router;
export default router;

View File

@@ -168,6 +168,20 @@ router.get(
integrationAuthController.getIntegrationAuthNorthflankSecretGroups
);
router.get(
"/:integrationAuthId/teamcity/build-configs",
requireAuth({
acceptedAuthModes: [AuthMode.JWT],
}),
requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER],
}),
param("integrationAuthId").exists().isString(),
query("appId").exists().isString(),
validateRequest,
integrationAuthController.getIntegrationAuthTeamCityBuildConfigs
);
router.delete(
"/:integrationAuthId",
requireAuth({

View File

@@ -1,4 +1,4 @@
import express, { Request, Response } from "express";
import express from "express";
const router = express.Router();
import { requireAuth, requireWorkspaceAuth, validateRequest } from "../../middleware";
import { body, param, query } from "express-validator";

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 936 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 942 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@@ -11,21 +11,18 @@ Prerequisites:
![integrations](../../images/integrations.png)
## Enter your TeamCity API Token and Server URL
## Enter your TeamCity Access Token and Server URL
Obtain a TeamCity API Token in Profile > Access Tokens
Obtain a TeamCity Access Token in Profile > Access Tokens
![integrations teamcity dashboard](../../images/integrations-teamcity-dashboard.png)
![integrations teamcity tokens](../../images/integrations-teamcity-tokens.png)
![integrations teamcity dashboard](../../images/integrations/teamcity/integrations-teamcity-dashboard.png)
![integrations teamcity token](../../images/integrations/teamcity/integrations-teamcity-token.png)
Obtain your TeamCity Server URL in Administration > Cloud Server Settings > Server URL
![integrations teamcity projects](../../images/integrations-teamcity-projects.png)
![integrations teamcity server url](../../images/integrations-teamcity-serverurl.png)
Press on the TeamCity tile and input your TeamCity API Token and Server URL to grant Infisical access to your TeamCity account.
![integrations teamcity authorization](../../images/integrations-teamcity-auth.png)
<Note>
For this integration to work, the TeamCity Access Token must either have the
**Same as current user** account-wide permission enabled or, if **Limit per project**
is selected, then it must at minimum have the **View build configuration settings** and **Edit project** permissions enabled.
</Note>
<Info>
If this is your project's first cloud integration, then you'll have to grant
@@ -34,9 +31,20 @@ Press on the TeamCity tile and input your TeamCity API Token and Server URL to g
the cloud platform.
</Info>
Press on the TeamCity tile and input your TeamCity Access Token and Server URL to grant Infisical access to your TeamCity account.
![integrations teamcity authorization](../../images/integrations/teamcity/integrations-teamcity-auth.png)
## Start integration
Select which Infisical environment secrets, you want to sync to which TeamCity project and press create integration to start syncing secrets to TeamCity.
Select which Infisical environment secrets you want to sync to which TeamCity project (and optionally build configuration) and press create integration to start syncing secrets to TeamCity.
![integrations teamcity](../../images/integrations-teamcity-create.png)
![integrations teamcity](../../images/integrations-teamcity.png)
![integrations teamcity](../../images/integrations/teamcity/integrations-teamcity-create.png)
<Note>
Infisical integrates with both TeamCity's project-level and build configuration-level environment variables.
To sync secrets to a specific build configuration in a TeamCity project, you can select a build configuration from the **TeamCity Build Config** dropdown; otherwise, leaving it empty will sync secrets to TeamCity at the project-level.
</Note>
![integrations teamcity](../../images/integrations/teamcity/integrations-teamcity.png)

View File

@@ -7,6 +7,7 @@ export {
useGetIntegrationAuthNorthflankSecretGroups,
useGetIntegrationAuthRailwayEnvironments,
useGetIntegrationAuthRailwayServices,
useGetIntegrationAuthTeamCityBuildConfigs,
useGetIntegrationAuthTeams,
useGetIntegrationAuthVercelBranches,
useSaveIntegrationAccessToken

View File

@@ -10,8 +10,8 @@ import {
IntegrationAuth,
NorthflankSecretGroup,
Service,
Team
} from "./types";
Team,
TeamCityBuildConfig} from "./types";
const integrationAuthKeys = {
getIntegrationAuthById: (integrationAuthId: string) =>
@@ -49,7 +49,14 @@ const integrationAuthKeys = {
}: {
integrationAuthId: string;
appId: string;
}) => [{ integrationAuthId, appId }, "integrationAuthNorthflankSecretGroups"] as const,
}) => [{ integrationAuthId, appId }, "integrationAuthNorthflankSecretGroups"] as const,
getIntegrationAuthTeamCityBuildConfigs: ({
integrationAuthId,
appId
}: {
integrationAuthId: string;
appId: string;
}) => [{ integrationAuthId, appId }, "integrationAuthTeamCityBranchConfigs"] as const,
};
const fetchIntegrationAuthById = async (integrationAuthId: string) => {
@@ -183,6 +190,27 @@ const fetchIntegrationAuthNorthflankSecretGroups = async ({
return secretGroups;
};
const fetchIntegrationAuthTeamCityBuildConfigs = async ({
integrationAuthId,
appId
}: {
integrationAuthId: string;
appId: string;
}) => {
const {
data: { buildConfigs }
} = await apiRequest.get<{ buildConfigs: TeamCityBuildConfig[] }>(
`/api/v1/integration-auth/${integrationAuthId}/teamcity/build-configs`,
{
params: {
appId
}
}
);
return buildConfigs;
};
export const useGetIntegrationAuthById = (integrationAuthId: string) => {
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthById(integrationAuthId),
@@ -312,6 +340,26 @@ export const useGetIntegrationAuthNorthflankSecretGroups = ({
});
};
export const useGetIntegrationAuthTeamCityBuildConfigs = ({
integrationAuthId,
appId
}: {
integrationAuthId: string;
appId: string;
}) => {
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthTeamCityBuildConfigs({
integrationAuthId,
appId
}),
queryFn: () => fetchIntegrationAuthTeamCityBuildConfigs({
integrationAuthId,
appId
}),
enabled: true
});
};
export const useAuthorizeIntegration = () => {
const queryClient = useQueryClient();

View File

@@ -40,4 +40,9 @@ export type BitBucketWorkspace = {
export type NorthflankSecretGroup = {
name: string;
groupId: string;
}
export type TeamCityBuildConfig = {
name: string;
buildConfigId: string;
}

View File

@@ -17,13 +17,20 @@ import {
} from "../../../components/v2";
import {
useGetIntegrationAuthApps,
useGetIntegrationAuthById
useGetIntegrationAuthById,
useGetIntegrationAuthTeamCityBuildConfigs
} from "../../../hooks/api/integrationAuth";
import { useGetWorkspaceById } from "../../../hooks/api/workspace";
export default function TeamCityCreateIntegrationPage() {
const router = useRouter();
const { mutateAsync } = useCreateIntegration();
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
const [targetAppId, setTargetAppId] = useState("");
const [targetBuildConfigId, setTargetBuildConfigId] = useState<string>("");
const [secretPath, setSecretPath] = useState("/");
const [isLoading, setIsLoading] = useState(false);
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]);
@@ -32,11 +39,11 @@ export default function TeamCityCreateIntegrationPage() {
const { data: integrationAuthApps } = useGetIntegrationAuthApps({
integrationAuthId: (integrationAuthId as string) ?? ""
});
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
const [targetApp, setTargetApp] = useState("");
const [secretPath, setSecretPath] = useState("/");
const [isLoading, setIsLoading] = useState(false);
const { data: targetBuildConfigs } = useGetIntegrationAuthTeamCityBuildConfigs({
integrationAuthId: (integrationAuthId as string) ?? "",
appId: targetAppId
});
useEffect(() => {
if (workspace) {
@@ -47,29 +54,31 @@ export default function TeamCityCreateIntegrationPage() {
useEffect(() => {
if (integrationAuthApps) {
if (integrationAuthApps.length > 0) {
setTargetApp(integrationAuthApps[0].name);
setTargetAppId(integrationAuthApps[0].appId as string);
} else {
setTargetApp("none");
setTargetAppId("none");
}
}
}, [integrationAuthApps]);
const handleButtonClick = async () => {
try {
if (!integrationAuth?._id) return;
setIsLoading(true);
const targetEnvironment = targetBuildConfigs?.find(
(buildConfig) => buildConfig.buildConfigId === targetBuildConfigId
);
await mutateAsync({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: targetApp,
appId:
integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp)
?.appId ?? null,
app: integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.appId === targetAppId)?.name ?? null,
appId: targetAppId,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
targetEnvironmentId: null,
targetEnvironment: targetEnvironment ? targetEnvironment.name : null,
targetEnvironmentId: targetEnvironment ? targetEnvironment.buildConfigId : null,
targetService: null,
targetServiceId: null,
owner: null,
@@ -86,12 +95,17 @@ export default function TeamCityCreateIntegrationPage() {
}
};
const filteredBuildConfigs = targetBuildConfigs?.concat({
name: "",
buildConfigId: ""
});
return integrationAuth &&
workspace &&
selectedSourceEnvironment &&
integrationAuthApps &&
targetApp ? (
filteredBuildConfigs &&
targetAppId ? (
<div className="flex h-full w-full items-center justify-center">
<Card className="max-w-md rounded-md p-8">
<CardTitle className="text-center">TeamCity Integration</CardTitle>
@@ -120,16 +134,16 @@ export default function TeamCityCreateIntegrationPage() {
</FormControl>
<FormControl label="TeamCity Project" className="mt-4">
<Select
value={targetApp}
onValueChange={(val) => setTargetApp(val)}
value={targetAppId}
onValueChange={(val) => setTargetAppId(val)}
className="w-full border border-mineshaft-500"
isDisabled={integrationAuthApps.length === 0}
>
{integrationAuthApps.length > 0 ? (
integrationAuthApps.map((integrationAuthApp) => (
<SelectItem
value={integrationAuthApp.name}
key={`target-app-${integrationAuthApp.name}`}
value={integrationAuthApp.appId as string}
key={`target-app-${integrationAuthApp.appId as string}`}
>
{integrationAuthApp.name}
</SelectItem>
@@ -141,6 +155,22 @@ export default function TeamCityCreateIntegrationPage() {
)}
</Select>
</FormControl>
<FormControl label="Team City Build Config (Optional)" className="mt-4">
<Select
value={targetBuildConfigId}
onValueChange={(val) => setTargetBuildConfigId(val)}
className="w-full border border-mineshaft-500"
>
{filteredBuildConfigs.map((buildConfig: any) => (
<SelectItem
value={buildConfig.buildConfigId}
key={`target-build-config-${buildConfig.buildConfigId}`}
>
{buildConfig.name}
</SelectItem>
))}
</Select>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"

View File

@@ -106,6 +106,7 @@ export const IntegrationsSection = ({
integration.integration === "netlify" ||
integration.integration === "railway" ||
integration.integration === "gitlab" ||
integration.integration === "teamcity" ||
integration.integration === "bitbucket") && (
<div className="ml-4 flex flex-col">
<FormLabel label="Target Environment" />