Compare commits

..

4 Commits

Author SHA1 Message Date
da561e37c5 Fix: Backwards compatibility and UI fixes 2024-09-05 21:43:10 +04:00
a29fb613b9 Requested changes 2024-09-05 18:48:20 +04:00
8f3d328b9a Update integration-sync-secret.ts 2024-09-05 13:38:31 +04:00
b7d683ee1b fix(integrations/circle-ci): Refactored Circle CI integration
The integration seemingly never worked in the first place due to inpropper project slugs. This PR resolves it.
2024-09-05 13:30:20 +04:00
7 changed files with 175 additions and 67 deletions

View File

@ -42,7 +42,7 @@ export const identityUaClientSecretDALFactory = (db: TDbClient) => {
})
.orWhere((qb) => {
void qb
.where("clientSecretNumUsesLimit", ">", 0)
.where("clientSecretNumUses", ">", 0)
.andWhere(
"clientSecretNumUses",
">=",

View File

@ -460,16 +460,21 @@ const getAppsFlyio = async ({ accessToken }: { accessToken: string }) => {
*/
const getAppsCircleCI = async ({ accessToken }: { accessToken: string }) => {
const res = (
await request.get<{ reponame: string }[]>(`${IntegrationUrls.CIRCLECI_API_URL}/v1.1/projects`, {
headers: {
"Circle-Token": accessToken,
"Accept-Encoding": "application/json"
await request.get<{ reponame: string; username: string; vcs_url: string }[]>(
`${IntegrationUrls.CIRCLECI_API_URL}/v1.1/projects`,
{
headers: {
"Circle-Token": accessToken,
"Accept-Encoding": "application/json"
}
}
})
)
).data;
const apps = res?.map((a) => ({
name: a?.reponame
const apps = res.map((a) => ({
owner: a.username, // username maps to unique organization name in CircleCI
name: a.reponame, // reponame maps to project name within an organization in CircleCI
appId: a.vcs_url.split("/").pop() // vcs_url maps to the project id in CircleCI
}));
return apps;

View File

@ -1929,22 +1929,62 @@ const syncSecretsCircleCI = async ({
secrets: Record<string, { value: string; comment?: string }>;
accessToken: string;
}) => {
const circleciOrganizationDetail = (
await request.get(`${IntegrationUrls.CIRCLECI_API_URL}/v2/me/collaborations`, {
const getProjectSlug = async () => {
const requestConfig = {
headers: {
"Circle-Token": accessToken,
"Accept-Encoding": "application/json"
}
})
).data[0];
};
const { slug } = circleciOrganizationDetail;
try {
const projectDetails = (
await request.get<{ slug: string }>(
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${integration.appId}`,
requestConfig
)
).data;
return projectDetails.slug;
} catch (err) {
if (err instanceof AxiosError) {
if (err.response?.data?.message !== "Not Found") {
throw new Error("Failed to get project slug from CircleCI during first attempt.");
}
}
}
// For backwards compatibility with old CircleCI integrations where we don't keep track of the organization name, so we can't filter by organization
try {
const circleCiOrganization = (
await request.get<{ slug: string; name: string }[]>(
`${IntegrationUrls.CIRCLECI_API_URL}/v2/me/collaborations`,
requestConfig
)
).data;
// Case 1: This is a new integration where the organization name is stored under `integration.owner`
if (integration.owner) {
const org = circleCiOrganization.find((o) => o.name === integration.owner);
if (org) {
return `${org.slug}/${integration.app}`;
}
}
// Case 2: This is an old integration where the organization name is not stored, so we have to assume the first organization is the correct one
return `${circleCiOrganization[0].slug}/${integration.app}`;
} catch (err) {
throw new Error("Failed to get project slug from CircleCI during second attempt.");
}
};
const projectSlug = await getProjectSlug();
// sync secrets to CircleCI
await Promise.all(
Object.keys(secrets).map(async (key) =>
request.post(
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar`,
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${projectSlug}/envvar`,
{
name: key,
value: secrets[key].value
@ -1962,7 +2002,7 @@ const syncSecretsCircleCI = async ({
// get secrets from CircleCI
const getSecretsRes = (
await request.get<{ items: { name: string }[] }>(
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar`,
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${projectSlug}/envvar`,
{
headers: {
"Circle-Token": accessToken,
@ -1976,15 +2016,12 @@ const syncSecretsCircleCI = async ({
await Promise.all(
getSecretsRes.map(async (sec) => {
if (!(sec.name in secrets)) {
return request.delete(
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar/${sec.name}`,
{
headers: {
"Circle-Token": accessToken,
"Content-Type": "application/json"
}
return request.delete(`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${projectSlug}/envvar/${sec.name}`, {
headers: {
"Circle-Token": accessToken,
"Content-Type": "application/json"
}
);
});
}
})
);

View File

@ -1,9 +1,9 @@
---
title: "Elasticsearch"
description: "Learn how to dynamically generate Elasticsearch user credentials."
title: "Elastic Search"
description: "Learn how to dynamically generate Elastic Search user credentials."
---
The Infisical Elasticsearch dynamic secret allows you to generate Elasticsearch credentials on demand based on configured role.
The Infisical Elastic Search dynamic secret allows you to generate Elastic Search credentials on demand based on configured role.
## Prerequisites
@ -16,7 +16,7 @@ The Infisical Elasticsearch dynamic secret allows you to generate Elasticsearch
For testing purposes, you can also use a highly privileged role like `superuser`, that will have full control over the cluster. This is not recommended in production environments following the principle of least privilege.
</Note>
## Set up Dynamic Secrets with Elasticsearch
## Set up Dynamic Secrets with Elastic Search
<Steps>
<Step title="Open Secret Overview Dashboard">
@ -25,7 +25,7 @@ The Infisical Elasticsearch dynamic secret allows you to generate Elasticsearch
<Step title="Click on the 'Add Dynamic Secret' button">
![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button-redis.png)
</Step>
<Step title="Select 'Elasticsearch'">
<Step title="Select 'Elastic Search'">
![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/dynamic-secret-modal-elastic-search.png)
</Step>
<Step title="Provide the inputs for dynamic secret parameters">
@ -42,11 +42,11 @@ The Infisical Elasticsearch dynamic secret allows you to generate Elasticsearch
</ParamField>
<ParamField path="Host" type="string" required>
Your Elasticsearch host. This is the endpoint that your instance runs on. _(Example: https://your-cluster-ip)_
Your Elastic Search host. This is the endpoint that your instance runs on. _(Example: https://your-cluster-ip)_
</ParamField>
<ParamField path="Port" type="string" required>
The port that your Elasticsearch instance is running on. _(Example: 9200)_
The port that your Elastic Search instance is running on. _(Example: 9200)_
</ParamField>
<ParamField path="Roles" type="string[]" required>
@ -54,7 +54,7 @@ The Infisical Elasticsearch dynamic secret allows you to generate Elasticsearch
</ParamField>
<ParamField path="Authentication Method" type="API Key | Username/Password" required>
Select the authentication method you want to use to connect to your Elasticsearch instance.
Select the authentication method you want to use to connect to your Elastic Search instance.
</ParamField>
<ParamField path="Username" type="string" required>

View File

@ -69,7 +69,7 @@ export default function AWSParameterStoreAuthorizeIntegrationPage() {
subTitle="After adding the details below, you will be prompted to set up an integration for a particular Infisical project and environment."
>
<div className="flex flex-row items-center">
<div className="inline flex items-center">
<div className="flex items-center">
<Image
src="/images/integrations/Amazon Web Services.png"
height={35}

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import Head from "next/head";
import Image from "next/image";
import Link from "next/link";
@ -12,6 +12,7 @@ import {
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import queryString from "query-string";
import { createNotification } from "@app/components/notifications";
import { useCreateIntegration } from "@app/hooks/api";
import {
@ -45,9 +46,10 @@ export default function CircleCICreateIntegrationPage() {
});
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
const [targetOrganization, setTargetOrganization] = useState("");
const [secretPath, setSecretPath] = useState("/");
const [targetApp, setTargetApp] = useState("");
const [targetProjectId, setTargetProjectId] = useState("");
const [isLoading, setIsLoading] = useState(false);
@ -57,29 +59,40 @@ export default function CircleCICreateIntegrationPage() {
}
}, [workspace]);
useEffect(() => {
if (integrationAuthApps) {
if (integrationAuthApps.length > 0) {
setTargetApp(integrationAuthApps[0]?.name);
} else {
setTargetApp("none");
}
}
}, [integrationAuthApps]);
const handleButtonClick = async () => {
try {
if (!integrationAuth?.id) return;
if (!targetProjectId || targetOrganization === "none") {
createNotification({
type: "error",
text: "Please select a project"
});
setIsLoading(false);
return;
}
setIsLoading(true);
const selectedApp = integrationAuthApps?.find(
(integrationAuthApp) => integrationAuthApp.appId === targetProjectId
);
if (!selectedApp) {
createNotification({
type: "error",
text: "Invalid project selected"
});
setIsLoading(false);
return;
}
await mutateAsync({
integrationAuthId: integrationAuth?.id,
isActive: true,
app: targetApp,
appId: integrationAuthApps?.find(
(integrationAuthApp) => integrationAuthApp.name === targetApp
)?.appId,
app: selectedApp.name, // project name
owner: selectedApp.owner, // organization name
appId: selectedApp.appId, // project id (used for syncing)
sourceEnvironment: selectedSourceEnvironment,
secretPath
});
@ -92,11 +105,28 @@ export default function CircleCICreateIntegrationPage() {
}
};
return integrationAuth &&
workspace &&
selectedSourceEnvironment &&
integrationAuthApps &&
targetApp ? (
const filteredProjects = useMemo(() => {
if (!integrationAuthApps) return [];
return integrationAuthApps.filter((integrationAuthApp) => {
return integrationAuthApp.owner === targetOrganization;
});
}, [integrationAuthApps, targetOrganization]);
const filteredOrganizations = useMemo(() => {
const organizations = new Set<string>();
if (integrationAuthApps) {
integrationAuthApps.forEach((integrationAuthApp) => {
if (!integrationAuthApp.owner) return;
organizations.add(integrationAuthApp.owner);
});
}
return Array.from(organizations);
}, [integrationAuthApps]);
return integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps ? (
<div className="flex h-full w-full flex-col items-center justify-center">
<Head>
<title>Set Up CircleCI Integration</title>
@ -108,7 +138,7 @@ export default function CircleCICreateIntegrationPage() {
subTitle="Choose which environment or folder in Infisical you want to sync to CircleCI environment variables."
>
<div className="flex flex-row items-center">
<div className="inline flex items-center pb-0.5">
<div className="flex items-center pb-0.5">
<Image
src="/images/integrations/Circle CI.png"
height={30}
@ -131,6 +161,7 @@ export default function CircleCICreateIntegrationPage() {
</Link>
</div>
</CardTitle>
<FormControl label="Project Environment" className="px-6">
<Select
value={selectedSourceEnvironment}
@ -154,29 +185,55 @@ export default function CircleCICreateIntegrationPage() {
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="CircleCI Project" className="px-6">
<FormControl label="CircleCI Organization" className="px-6">
<Select
value={targetApp}
onValueChange={(val) => setTargetApp(val)}
value={targetOrganization}
onValueChange={(val) => {
setTargetOrganization(val);
setTargetProjectId("none");
}}
className="w-full border border-mineshaft-500"
isDisabled={integrationAuthApps.length === 0}
isDisabled={filteredOrganizations.length === 0}
>
{integrationAuthApps.length > 0 ? (
integrationAuthApps.map((integrationAuthApp) => (
<SelectItem
value={integrationAuthApp.name}
key={`target-app-${integrationAuthApp.name}`}
>
{integrationAuthApp.name}
{filteredOrganizations.length > 0 ? (
filteredOrganizations.map((org) => (
<SelectItem value={org} key={`target-org-${org}`}>
{org}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-app-none">
No projects found
No organizations found
</SelectItem>
)}
</Select>
</FormControl>
{targetOrganization && (
<FormControl label="CircleCI Project ID" className="px-6">
<Select
value={targetProjectId}
onValueChange={(val) => {
setTargetProjectId(val);
}}
className="w-full border border-mineshaft-500"
isDisabled={filteredProjects.length === 0}
>
{filteredProjects.length > 0 ? (
filteredProjects.map((project) => (
<SelectItem value={project.appId!} key={`target-project-${project.owner}`}>
{project.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-app-none">
No projects found
</SelectItem>
)}
</Select>
</FormControl>
)}
<Button
onClick={handleButtonClick}
colorSchema="primary"

View File

@ -99,6 +99,7 @@ export const ConfiguredIntegrationItem = ({
<FormLabel
label={
(integration.integration === "qovery" && integration?.scope) ||
(integration.integration === "circleci" && "Project") ||
(integration.integration === "aws-secret-manager" && "Secret") ||
(["aws-parameter-store", "rundeck"].includes(integration.integration) && "Path") ||
(integration?.integration === "terraform-cloud" && "Project") ||
@ -142,6 +143,14 @@ export const ConfiguredIntegrationItem = ({
</div>
</div>
)}
{integration.integration === "circleci" && integration.owner && (
<div className="ml-2">
<FormLabel label="Organization" />
<div className="rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
{integration.owner}
</div>
</div>
)}
{integration.integration === "terraform-cloud" && integration.targetService && (
<div className="ml-2">
<FormLabel label="Category" />