Merge pull request #806 from atimapreandrew/teamcity-integration

Teamcity integration
This commit is contained in:
BlackMagiq
2023-08-01 19:04:20 +07:00
committed by GitHub
21 changed files with 443 additions and 2 deletions

View File

@ -35,6 +35,7 @@ import {
INTEGRATION_RENDER_API_URL,
INTEGRATION_SUPABASE,
INTEGRATION_SUPABASE_API_URL,
INTEGRATION_TEAMCITY,
INTEGRATION_TERRAFORM_CLOUD,
INTEGRATION_TERRAFORM_CLOUD_API_URL,
INTEGRATION_TRAVISCI,
@ -151,6 +152,12 @@ const getApps = async ({
accessToken,
});
break;
case INTEGRATION_TEAMCITY:
apps = await getAppsTeamCity({
integrationAuth,
accessToken,
});
break;
case INTEGRATION_SUPABASE:
apps = await getAppsSupabase({
accessToken,
@ -722,6 +729,39 @@ const getAppsGitlab = async ({
return apps;
};
/**
* Return list of projects for TeamCity integration
* @param {Object} obj
* @param {String} obj.accessToken - access token for TeamCity API
* @returns {Object[]} apps - names and ids of TeamCity projects
* @returns {String} apps.name - name of TeamCity projects
*/
const getAppsTeamCity = async ({
integrationAuth,
accessToken,
}: {
integrationAuth: IIntegrationAuth;
accessToken: string;
}) => {
const res = (
await standardRequest.get(`${integrationAuth.url}/app/rest/projects`, {
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json",
},
})
).data.project.slice(1);
const apps = res.map((a: any) => {
return {
name: a.name,
appId: a.id,
};
});
return apps;
};
/**
* Return list of projects for Supabase integration
* @param {Object} obj

View File

@ -44,6 +44,7 @@ import {
INTEGRATION_RENDER_API_URL,
INTEGRATION_SUPABASE,
INTEGRATION_SUPABASE_API_URL,
INTEGRATION_TEAMCITY,
INTEGRATION_TERRAFORM_CLOUD,
INTEGRATION_TERRAFORM_CLOUD_API_URL,
INTEGRATION_TRAVISCI,
@ -233,6 +234,14 @@ const syncSecrets = async ({
accessToken,
});
break;
case INTEGRATION_TEAMCITY:
await syncSecretsTeamCity({
integrationAuth,
integration,
secrets,
accessToken,
});
break;
case INTEGRATION_BITBUCKET:
await syncSecretsBitBucket({
integration,
@ -1971,6 +1980,89 @@ const syncSecretsTerraformCloud = async ({
}
};
/**
* Sync/push [secrets] to TeamCity project
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {Object} obj.secrets - secrets to push to integration
* @param {String} obj.accessToken - access token for TeamCity integration
*/
const syncSecretsTeamCity = async ({
integrationAuth,
integration,
secrets,
accessToken,
}: {
integrationAuth: IIntegrationAuth;
integration: IIntegration;
secrets: any;
accessToken: string;
}) => {
interface TeamCitySecret {
name: string;
value: string;
}
// get secrets from Teamcity
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]
},
{
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"
}
}
);
}
}
};
/**
* Sync/push [secrets] to HashiCorp Vault path
* @param {Object} obj

View File

@ -20,6 +20,7 @@ import {
INTEGRATION_RAILWAY,
INTEGRATION_RENDER,
INTEGRATION_SUPABASE,
INTEGRATION_TEAMCITY,
INTEGRATION_TERRAFORM_CLOUD,
INTEGRATION_TRAVISCI,
INTEGRATION_VERCEL,
@ -61,6 +62,7 @@ export interface IIntegration {
| "supabase"
| "checkly"
| "terraform-cloud"
| "teamcity"
| "hashicorp-vault"
| "cloudflare-pages"
| "bitbucket"
@ -157,6 +159,7 @@ const integrationSchema = new Schema<IIntegration>(
INTEGRATION_SUPABASE,
INTEGRATION_CHECKLY,
INTEGRATION_TERRAFORM_CLOUD,
INTEGRATION_TEAMCITY,
INTEGRATION_HASHICORP_VAULT,
INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_CODEFRESH,

View File

@ -22,6 +22,7 @@ import {
INTEGRATION_RAILWAY,
INTEGRATION_RENDER,
INTEGRATION_SUPABASE,
INTEGRATION_TEAMCITY,
INTEGRATION_TERRAFORM_CLOUD,
INTEGRATION_TRAVISCI,
INTEGRATION_VERCEL,
@ -55,6 +56,7 @@ export interface IIntegrationAuth extends Document {
| "bitbucket"
| "cloud-66"
| "terraform-cloud"
| "teamcity"
| "northflank"
| "windmill";
teamId: string;
@ -99,6 +101,7 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
INTEGRATION_CIRCLECI,
INTEGRATION_LARAVELFORGE,
INTEGRATION_TRAVISCI,
INTEGRATION_TEAMCITY,
INTEGRATION_SUPABASE,
INTEGRATION_TERRAFORM_CLOUD,
INTEGRATION_HASHICORP_VAULT,

View File

@ -23,6 +23,7 @@ export const INTEGRATION_FLYIO = "flyio";
export const INTEGRATION_LARAVELFORGE = "laravel-forge"
export const INTEGRATION_CIRCLECI = "circleci";
export const INTEGRATION_TRAVISCI = "travisci";
export const INTEGRATION_TEAMCITY = "teamcity";
export const INTEGRATION_SUPABASE = "supabase";
export const INTEGRATION_CHECKLY = "checkly";
export const INTEGRATION_TERRAFORM_CLOUD = "terraform-cloud";
@ -46,6 +47,7 @@ export const INTEGRATION_SET = new Set([
INTEGRATION_CIRCLECI,
INTEGRATION_LARAVELFORGE,
INTEGRATION_TRAVISCI,
INTEGRATION_TEAMCITY,
INTEGRATION_SUPABASE,
INTEGRATION_CHECKLY,
INTEGRATION_TERRAFORM_CLOUD,
@ -233,6 +235,15 @@ export const getIntegrationOptions = async () => {
clientId: "",
docsLink: "",
},
{
name: "TeamCity",
slug: "teamcity",
image: "TeamCity.png",
isAvailable: true,
type: "pat",
clientId: "",
docsLink: "",
},
{
name: "Supabase",
slug: "supabase",

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 228 KiB

After

Width:  |  Height:  |  Size: 228 KiB

View File

@ -0,0 +1,42 @@
---
title: "TeamCity"
description: "How to sync secrets from Infisical to TeamCity"
---
Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
## Navigate to your project's integrations tab
![integrations](../../images/integrations.png)
## Enter your TeamCity API Token and Server URL
Obtain a TeamCity API Token in Profile > Access Tokens
![integrations teamcity dashboard](../../images/integrations-teamcity-dashboard.png)
![integrations teamcity tokens](../../images/integrations-teamcity-tokens.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)
<Info>
If this is your project's first cloud integration, then you'll have to grant
Infisical access to your project's environment variables. Although this step
breaks E2EE, it's necessary for Infisical to sync the environment variables to
the cloud platform.
</Info>
## 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.
![integrations teamcity](../../images/integrations-teamcity-create.png)
![integrations teamcity](../../images/integrations-teamcity.png)

View File

@ -21,6 +21,7 @@ Missing an integration? [Throw in a request](https://github.com/Infisical/infisi
| [Laravel Forge](/integrations/cloud/laravel-forge) | Cloud | Available |
| [Railway](/integrations/cloud/railway) | Cloud | Available |
| [Terraform Cloud](/integrations/cloud/terraform-cloud) | Cloud | Available |
| [TeamCity](/integrations/cloud/teamcity) | Cloud | Available |
| [Fly.io](/integrations/cloud/flyio) | Cloud | Available |
| [Supabase](/integrations/cloud/supabase) | Cloud | Available |
| [Northflank](/integrations/cloud/northflank) | Cloud | Available |

View File

@ -232,6 +232,7 @@
"integrations/cloud/supabase",
"integrations/cloud/northflank",
"integrations/cloud/terraform-cloud",
"integrations/cloud/teamcity",
"integrations/cloud/cloudflare-pages",
"integrations/cloud/checkly",
"integrations/cloud/hashicorp-vault",

View File

@ -19,7 +19,8 @@ const integrationSlugNameMapping: Mapping = {
travisci: "TravisCI",
supabase: "Supabase",
checkly: "Checkly",
'terraform-cloud': 'Terraform Cloud',
"terraform-cloud": "Terraform Cloud",
"teamcity": "TeamCity",
"hashicorp-vault": "Vault",
"cloudflare-pages": "Cloudflare Pages",
"codefresh": "Codefresh",
@ -27,7 +28,7 @@ const integrationSlugNameMapping: Mapping = {
bitbucket: "BitBucket",
"cloud-66": "Cloud 66",
northflank: "Northflank",
'windmill': 'Windmill'
"windmill": "Windmill"
}
const envMapping: Mapping = {

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

View File

@ -0,0 +1,88 @@
import { useState } from "react";
import { useRouter } from "next/router";
import { Button, Card, CardTitle, FormControl, Input } from "../../../components/v2";
import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken";
export default function TeamCityCreateIntegrationPage() {
const router = useRouter();
const [apiKey, setApiKey] = useState("");
const [apiKeyErrorText, setApiKeyErrorText] = useState("");
const [serverUrl, setServerUrl] = useState("");
const [serverUrlErrorText, setServerUrlErrorText] = useState("");
const [isLoading, setIsLoading] = useState(false);
const handleButtonClick = async () => {
try {
setApiKeyErrorText("");
setServerUrlErrorText("");
if (apiKey.length === 0) {
setApiKeyErrorText("Access Token cannot be blank");
return;
}
if (serverUrl.length === 0) {
setServerUrlErrorText("Server URL cannot be blank");
return;
}
setIsLoading(true);
const integrationAuth = await saveIntegrationAccessToken({
workspaceId: localStorage.getItem("projectData.id"),
integration: "teamcity",
accessId: null,
accessToken: apiKey,
url: serverUrl,
namespace: null
});
setIsLoading(false);
router.push(`/integrations/teamcity/create?integrationAuthId=${integrationAuth._id}`);
} catch (err) {
console.error(err);
}
};
return (
<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>
<FormControl
label="TeamCity Access Token"
errorText={apiKeyErrorText}
isError={apiKeyErrorText !== "" ?? false}
>
<Input
placeholder="Access Token"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
/>
</FormControl>
<FormControl
label="TeamCity Server URL"
errorText={serverUrlErrorText}
isError={serverUrlErrorText !== "" ?? false}
>
<Input
placeholder="https://example.teamcity.com"
value={serverUrl}
onChange={(e) => setServerUrl(e.target.value)}
/>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className="mt-4"
isLoading={isLoading}
>
Connect to TeamCity
</Button>
</Card>
</div>
);
}
TeamCityCreateIntegrationPage.requireAuth = true;

View File

@ -0,0 +1,156 @@
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import queryString from "query-string";
import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from "../../../components/v2";
import {
useGetIntegrationAuthApps,
useGetIntegrationAuthById
} from "../../../hooks/api/integrationAuth";
import { useGetWorkspaceById } from "../../../hooks/api/workspace";
import createIntegration from "../../api/integrations/createIntegration";
export default function TeamCityCreateIntegrationPage() {
const router = useRouter();
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]);
const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? "");
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? "");
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);
useEffect(() => {
if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug);
}
}, [workspace]);
useEffect(() => {
if (integrationAuthApps) {
if (integrationAuthApps.length > 0) {
setTargetApp(integrationAuthApps[0].name);
} else {
setTargetApp("none");
}
}
}, [integrationAuthApps]);
const handleButtonClick = async () => {
try {
if (!integrationAuth?._id) return;
setIsLoading(true);
await createIntegration({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: targetApp,
appId:
integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp)
?.appId ?? null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
targetEnvironmentId: null,
targetService: null,
targetServiceId: null,
owner: null,
path: null,
region: null,
secretPath
});
setIsLoading(false);
router.push(`/integrations/${localStorage.getItem("projectData.id")}`);
} catch (err) {
console.error(err);
}
};
return integrationAuth &&
workspace &&
selectedSourceEnvironment &&
integrationAuthApps &&
targetApp ? (
<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>
<FormControl label="Project Environment" className="mt-4">
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className="w-full border border-mineshaft-500"
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem
value={sourceEnvironment.slug}
key={`source-environment-${sourceEnvironment.slug}`}
>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="TeamCity Project" className="mt-4">
<Select
value={targetApp}
onValueChange={(val) => setTargetApp(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}`}
>
{integrationAuthApp.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-app-none">
No project found
</SelectItem>
)}
</Select>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className="mt-4"
isLoading={isLoading}
isDisabled={integrationAuthApps.length === 0}
>
Create Integration
</Button>
</Card>
</div>
) : (
<div />
);
}
TeamCityCreateIntegrationPage.requireAuth = true;

View File

@ -113,6 +113,9 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) =>
case "windmill":
link = `${window.location.origin}/integrations/windmill/authorize`;
break;
case "teamcity":
link = `${window.location.origin}/integrations/teamcity/authorize`;
break;
default:
break;
}