mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 07:26:45 +00:00
feat(integrations): New CircleCI Context Sync
This commit is contained in:
@@ -1123,4 +1123,38 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
|
|||||||
return { spaces };
|
return { spaces };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:integrationAuthId/circleci/organizations",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
integrationAuthId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
organizations: z
|
||||||
|
.object({
|
||||||
|
name: z.string(),
|
||||||
|
slug: z.string()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const organizations = await server.services.integrationAuth.getCircleCIOrganizations({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
id: req.params.integrationAuthId
|
||||||
|
});
|
||||||
|
return { organizations };
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { getConfig } from "@app/lib/config/env";
|
|||||||
import { request } from "@app/lib/config/request";
|
import { request } from "@app/lib/config/request";
|
||||||
import { NotFoundError } from "@app/lib/errors";
|
import { NotFoundError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { TCircleCIContext } from "./integration-app-types";
|
||||||
import { IntegrationAuthMetadataSchema, TIntegrationAuthMetadata } from "./integration-auth-schema";
|
import { IntegrationAuthMetadataSchema, TIntegrationAuthMetadata } from "./integration-auth-schema";
|
||||||
import { Integrations, IntegrationUrls } from "./integration-list";
|
import { Integrations, IntegrationUrls } from "./integration-list";
|
||||||
|
|
||||||
@@ -489,6 +490,47 @@ const getAppsCircleCI = async ({ accessToken }: { accessToken: string }) => {
|
|||||||
return apps;
|
return apps;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return list of contexts for CircleCI_Context integration
|
||||||
|
*/
|
||||||
|
const getAppsCircleCIContexts = async ({ accessToken, orgSlug }: { accessToken: string; orgSlug: string }) => {
|
||||||
|
type NextPageToken = string | null | undefined;
|
||||||
|
|
||||||
|
type CircleCIContextResponse = {
|
||||||
|
items: TCircleCIContext[];
|
||||||
|
next_page_token: NextPageToken;
|
||||||
|
};
|
||||||
|
|
||||||
|
const contexts: TCircleCIContext[] = [];
|
||||||
|
|
||||||
|
let nextPageToken: NextPageToken;
|
||||||
|
|
||||||
|
while (nextPageToken !== null) {
|
||||||
|
const res = (
|
||||||
|
await request.get<CircleCIContextResponse>(`${IntegrationUrls.CIRCLECI_CONTEXT_API_URL}/v2/context`, {
|
||||||
|
headers: {
|
||||||
|
"Circle-Token": accessToken,
|
||||||
|
"Accept-Encoding": "application/json"
|
||||||
|
},
|
||||||
|
params: new URLSearchParams({
|
||||||
|
"owner-slug": orgSlug,
|
||||||
|
...(nextPageToken ? { "page-token": nextPageToken } : {})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
|
||||||
|
contexts.push(...res.items);
|
||||||
|
nextPageToken = res.next_page_token;
|
||||||
|
}
|
||||||
|
|
||||||
|
const apps = contexts?.map((context) => ({
|
||||||
|
name: context.name,
|
||||||
|
appId: context.id
|
||||||
|
}));
|
||||||
|
|
||||||
|
return apps;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return list of projects for Databricks integration
|
* Return list of projects for Databricks integration
|
||||||
*/
|
*/
|
||||||
@@ -1195,6 +1237,12 @@ export const getApps = async ({
|
|||||||
accessToken
|
accessToken
|
||||||
});
|
});
|
||||||
|
|
||||||
|
case Integrations.CIRCLECI_CONTEXT:
|
||||||
|
return getAppsCircleCIContexts({
|
||||||
|
accessToken,
|
||||||
|
orgSlug: workspaceSlug as string
|
||||||
|
});
|
||||||
|
|
||||||
case Integrations.DATABRICKS:
|
case Integrations.DATABRICKS:
|
||||||
return getAppsDatabricks({
|
return getAppsDatabricks({
|
||||||
url,
|
url,
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export type TCircleCIContext = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
created_at: string;
|
||||||
|
};
|
||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
TBitbucketEnvironment,
|
TBitbucketEnvironment,
|
||||||
TBitbucketWorkspace,
|
TBitbucketWorkspace,
|
||||||
TChecklyGroups,
|
TChecklyGroups,
|
||||||
|
TCircleCIOrganization,
|
||||||
TDeleteIntegrationAuthByIdDTO,
|
TDeleteIntegrationAuthByIdDTO,
|
||||||
TDeleteIntegrationAuthsDTO,
|
TDeleteIntegrationAuthsDTO,
|
||||||
TDuplicateGithubIntegrationAuthDTO,
|
TDuplicateGithubIntegrationAuthDTO,
|
||||||
@@ -36,6 +37,7 @@ import {
|
|||||||
TIntegrationAuthBitbucketEnvironmentsDTO,
|
TIntegrationAuthBitbucketEnvironmentsDTO,
|
||||||
TIntegrationAuthBitbucketWorkspaceDTO,
|
TIntegrationAuthBitbucketWorkspaceDTO,
|
||||||
TIntegrationAuthChecklyGroupsDTO,
|
TIntegrationAuthChecklyGroupsDTO,
|
||||||
|
TIntegrationAuthCircleCIOrganizationDTO,
|
||||||
TIntegrationAuthGithubEnvsDTO,
|
TIntegrationAuthGithubEnvsDTO,
|
||||||
TIntegrationAuthGithubOrgsDTO,
|
TIntegrationAuthGithubOrgsDTO,
|
||||||
TIntegrationAuthHerokuPipelinesDTO,
|
TIntegrationAuthHerokuPipelinesDTO,
|
||||||
@@ -1427,6 +1429,40 @@ export const integrationAuthServiceFactory = ({
|
|||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getCircleCIOrganizations = async ({
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
id
|
||||||
|
}: TIntegrationAuthCircleCIOrganizationDTO) => {
|
||||||
|
const integrationAuth = await integrationAuthDAL.findById(id);
|
||||||
|
if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${id}' not found` });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
integrationAuth.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
|
||||||
|
const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId);
|
||||||
|
const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
|
||||||
|
|
||||||
|
const { data }: { data: TCircleCIOrganization[] } = await request.get(
|
||||||
|
`${IntegrationUrls.CIRCLECI_CONTEXT_API_URL}/v2/me/collaborations`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Circle-Token": `${accessToken}`,
|
||||||
|
"Accept-Encoding": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
const deleteIntegrationAuths = async ({
|
const deleteIntegrationAuths = async ({
|
||||||
projectId,
|
projectId,
|
||||||
integration,
|
integration,
|
||||||
@@ -1638,6 +1674,7 @@ export const integrationAuthServiceFactory = ({
|
|||||||
getTeamcityBuildConfigs,
|
getTeamcityBuildConfigs,
|
||||||
getBitbucketWorkspaces,
|
getBitbucketWorkspaces,
|
||||||
getBitbucketEnvironments,
|
getBitbucketEnvironments,
|
||||||
|
getCircleCIOrganizations,
|
||||||
getIntegrationAccessToken,
|
getIntegrationAccessToken,
|
||||||
duplicateIntegrationAuth,
|
duplicateIntegrationAuth,
|
||||||
getOctopusDeploySpaces,
|
getOctopusDeploySpaces,
|
||||||
|
|||||||
@@ -123,6 +123,10 @@ export type TGetIntegrationAuthTeamCityBuildConfigDTO = {
|
|||||||
appId: string;
|
appId: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TIntegrationAuthCircleCIOrganizationDTO = {
|
||||||
|
id: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TVercelBranches = {
|
export type TVercelBranches = {
|
||||||
ref: string;
|
ref: string;
|
||||||
lastCommit: string;
|
lastCommit: string;
|
||||||
@@ -184,6 +188,14 @@ export type TTeamCityBuildConfig = {
|
|||||||
webUrl: string;
|
webUrl: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TCircleCIOrganization = {
|
||||||
|
id: string;
|
||||||
|
vcsType: string;
|
||||||
|
name: string;
|
||||||
|
avatarUrl: string;
|
||||||
|
slug: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type TIntegrationsWithEnvironment = TIntegrations & {
|
export type TIntegrationsWithEnvironment = TIntegrations & {
|
||||||
environment?:
|
environment?:
|
||||||
| {
|
| {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export enum Integrations {
|
|||||||
FLYIO = "flyio",
|
FLYIO = "flyio",
|
||||||
LARAVELFORGE = "laravel-forge",
|
LARAVELFORGE = "laravel-forge",
|
||||||
CIRCLECI = "circleci",
|
CIRCLECI = "circleci",
|
||||||
|
CIRCLECI_CONTEXT = "circleci-context",
|
||||||
DATABRICKS = "databricks",
|
DATABRICKS = "databricks",
|
||||||
TRAVISCI = "travisci",
|
TRAVISCI = "travisci",
|
||||||
TEAMCITY = "teamcity",
|
TEAMCITY = "teamcity",
|
||||||
@@ -76,6 +77,8 @@ export enum IntegrationUrls {
|
|||||||
RAILWAY_API_URL = "https://backboard.railway.app/graphql/v2",
|
RAILWAY_API_URL = "https://backboard.railway.app/graphql/v2",
|
||||||
FLYIO_API_URL = "https://api.fly.io/graphql",
|
FLYIO_API_URL = "https://api.fly.io/graphql",
|
||||||
CIRCLECI_API_URL = "https://circleci.com/api",
|
CIRCLECI_API_URL = "https://circleci.com/api",
|
||||||
|
// eslint-disable-next-line
|
||||||
|
CIRCLECI_CONTEXT_API_URL = "https://circleci.com/api",
|
||||||
DATABRICKS_API_URL = "https:/xxxx.com/api",
|
DATABRICKS_API_URL = "https:/xxxx.com/api",
|
||||||
TRAVISCI_API_URL = "https://api.travis-ci.com",
|
TRAVISCI_API_URL = "https://api.travis-ci.com",
|
||||||
SUPABASE_API_URL = "https://api.supabase.com",
|
SUPABASE_API_URL = "https://api.supabase.com",
|
||||||
@@ -226,6 +229,15 @@ export const getIntegrationOptions = async () => {
|
|||||||
clientId: "",
|
clientId: "",
|
||||||
docsLink: ""
|
docsLink: ""
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Circle CI Contexts",
|
||||||
|
slug: "circleci-context",
|
||||||
|
image: "Circle CI.png",
|
||||||
|
isAvailable: true,
|
||||||
|
type: "pat",
|
||||||
|
clientId: "",
|
||||||
|
docsLink: ""
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Databricks",
|
name: "Databricks",
|
||||||
slug: "databricks",
|
slug: "databricks",
|
||||||
|
|||||||
@@ -2343,6 +2343,89 @@ const syncSecretsCircleCI = async ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync/push [secrets] to CircleCI Context
|
||||||
|
*/
|
||||||
|
const syncSecretsCircleCIContext = async ({
|
||||||
|
integration,
|
||||||
|
secrets,
|
||||||
|
accessToken
|
||||||
|
}: {
|
||||||
|
integration: TIntegrations;
|
||||||
|
secrets: Record<string, { value: string; comment?: string }>;
|
||||||
|
accessToken: string;
|
||||||
|
}) => {
|
||||||
|
// sync secrets to CircleCI
|
||||||
|
await Promise.all(
|
||||||
|
Object.keys(secrets).map(async (key) =>
|
||||||
|
request.put(
|
||||||
|
`${IntegrationUrls.CIRCLECI_API_URL}/v2/context/${integration.appId}/environment-variable/${key}`,
|
||||||
|
{
|
||||||
|
value: secrets[key].value
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Circle-Token": accessToken,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// get secrets from CircleCI
|
||||||
|
const getSecretsRes = async () => {
|
||||||
|
type EnvVars = {
|
||||||
|
variable: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
context_id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ResponseSchema = {
|
||||||
|
items: EnvVars[];
|
||||||
|
next_page_token: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
let nextPageToken: string | null | undefined;
|
||||||
|
const envVars: EnvVars[] = [];
|
||||||
|
|
||||||
|
while (nextPageToken !== null) {
|
||||||
|
const res = await request.get<ResponseSchema>(
|
||||||
|
`${IntegrationUrls.CIRCLECI_API_URL}/v2/context/${integration.appId}/environment-variable`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Circle-Token": accessToken,
|
||||||
|
"Accept-Encoding": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
envVars.push(...res.data.items);
|
||||||
|
nextPageToken = res.data.next_page_token;
|
||||||
|
}
|
||||||
|
|
||||||
|
return envVars;
|
||||||
|
};
|
||||||
|
|
||||||
|
// delete secrets from CircleCI
|
||||||
|
await Promise.all(
|
||||||
|
(await getSecretsRes()).map(async (sec) => {
|
||||||
|
if (!(sec.variable in secrets)) {
|
||||||
|
return request.delete(
|
||||||
|
`${IntegrationUrls.CIRCLECI_API_URL}/v2/context/${integration.appId}/environment-variable/${sec.variable}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Circle-Token": accessToken,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sync/push [secrets] to Databricks project
|
* Sync/push [secrets] to Databricks project
|
||||||
*/
|
*/
|
||||||
@@ -4433,6 +4516,13 @@ export const syncIntegrationSecrets = async ({
|
|||||||
accessToken
|
accessToken
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case Integrations.CIRCLECI_CONTEXT:
|
||||||
|
await syncSecretsCircleCIContext({
|
||||||
|
integration,
|
||||||
|
secrets,
|
||||||
|
accessToken
|
||||||
|
});
|
||||||
|
break;
|
||||||
case Integrations.DATABRICKS:
|
case Integrations.DATABRICKS:
|
||||||
await syncSecretsDatabricks({
|
await syncSecretsDatabricks({
|
||||||
integration,
|
integration,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const integrationSlugNameMapping: Mapping = {
|
|||||||
railway: "Railway",
|
railway: "Railway",
|
||||||
flyio: "Fly.io",
|
flyio: "Fly.io",
|
||||||
circleci: "CircleCI",
|
circleci: "CircleCI",
|
||||||
|
"circleci-context": "CircleCI Context",
|
||||||
databricks: "Databricks",
|
databricks: "Databricks",
|
||||||
travisci: "TravisCI",
|
travisci: "TravisCI",
|
||||||
supabase: "Supabase",
|
supabase: "Supabase",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export {
|
|||||||
useGetIntegrationAuthBitBucketWorkspaces,
|
useGetIntegrationAuthBitBucketWorkspaces,
|
||||||
useGetIntegrationAuthById,
|
useGetIntegrationAuthById,
|
||||||
useGetIntegrationAuthChecklyGroups,
|
useGetIntegrationAuthChecklyGroups,
|
||||||
|
useGetIntegrationAuthCircleCIOrganizations,
|
||||||
useGetIntegrationAuthGithubEnvs,
|
useGetIntegrationAuthGithubEnvs,
|
||||||
useGetIntegrationAuthGithubOrgs,
|
useGetIntegrationAuthGithubOrgs,
|
||||||
useGetIntegrationAuthNorthflankSecretGroups,
|
useGetIntegrationAuthNorthflankSecretGroups,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
BitBucketEnvironment,
|
BitBucketEnvironment,
|
||||||
BitBucketWorkspace,
|
BitBucketWorkspace,
|
||||||
ChecklyGroup,
|
ChecklyGroup,
|
||||||
|
CircleCIOrganization,
|
||||||
Environment,
|
Environment,
|
||||||
HerokuPipelineCoupling,
|
HerokuPipelineCoupling,
|
||||||
IntegrationAuth,
|
IntegrationAuth,
|
||||||
@@ -128,7 +129,9 @@ const integrationAuthKeys = {
|
|||||||
integrationAuthId,
|
integrationAuthId,
|
||||||
...params
|
...params
|
||||||
}: TGetIntegrationAuthOctopusDeployScopeValuesDTO) =>
|
}: TGetIntegrationAuthOctopusDeployScopeValuesDTO) =>
|
||||||
[{ integrationAuthId }, "getIntegrationAuthOctopusDeployScopeValues", params] as const
|
[{ integrationAuthId }, "getIntegrationAuthOctopusDeployScopeValues", params] as const,
|
||||||
|
getIntegrationAuthCircleCIOrganizations: (integrationAuthId: string) =>
|
||||||
|
[{ integrationAuthId }, "getIntegrationAuthCircleCIOrganizations"] as const
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchIntegrationAuthById = async (integrationAuthId: string) => {
|
const fetchIntegrationAuthById = async (integrationAuthId: string) => {
|
||||||
@@ -510,6 +513,15 @@ const fetchIntegrationAuthOctopusDeployScopeValues = async ({
|
|||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchIntegrationAuthCircleCIOrganizations = async (integrationAuthId: string) => {
|
||||||
|
const {
|
||||||
|
data: { organizations }
|
||||||
|
} = await apiRequest.get<{
|
||||||
|
organizations: CircleCIOrganization[];
|
||||||
|
}>(`/api/v1/integration-auth/${integrationAuthId}/circleci/organizations`);
|
||||||
|
return organizations;
|
||||||
|
};
|
||||||
|
|
||||||
export const useGetIntegrationAuthById = (integrationAuthId: string) => {
|
export const useGetIntegrationAuthById = (integrationAuthId: string) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: integrationAuthKeys.getIntegrationAuthById(integrationAuthId),
|
queryKey: integrationAuthKeys.getIntegrationAuthById(integrationAuthId),
|
||||||
@@ -884,6 +896,13 @@ export const useGetIntegrationAuthTeamCityBuildConfigs = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useGetIntegrationAuthCircleCIOrganizations = (integrationAuthId: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: integrationAuthKeys.getIntegrationAuthCircleCIOrganizations(integrationAuthId),
|
||||||
|
queryFn: () => fetchIntegrationAuthCircleCIOrganizations(integrationAuthId)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const useAuthorizeIntegration = () => {
|
export const useAuthorizeIntegration = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
|||||||
@@ -105,6 +105,11 @@ export enum OctopusDeployScope {
|
|||||||
// tenant, variable set
|
// tenant, variable set
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CircleCIOrganization = {
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type TGetIntegrationAuthOctopusDeployScopeValuesDTO = {
|
export type TGetIntegrationAuthOctopusDeployScopeValuesDTO = {
|
||||||
integrationAuthId: string;
|
integrationAuthId: string;
|
||||||
spaceId: string;
|
spaceId: string;
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import Head from "next/head";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2";
|
||||||
|
import { useSaveIntegrationAccessToken } from "@app/hooks/api";
|
||||||
|
|
||||||
|
export default function CircleCIContextCreateIntegrationPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const { mutateAsync } = useSaveIntegrationAccessToken();
|
||||||
|
|
||||||
|
const [apiKey, setApiKey] = useState("");
|
||||||
|
const [apiKeyErrorText, setApiKeyErrorText] = useState("");
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleButtonClick = async () => {
|
||||||
|
try {
|
||||||
|
setApiKeyErrorText("");
|
||||||
|
if (apiKey.length === 0) {
|
||||||
|
setApiKeyErrorText("API Key cannot be blank");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
const integrationAuth = await mutateAsync({
|
||||||
|
workspaceId: localStorage.getItem("projectData.id"),
|
||||||
|
integration: "circleci-context",
|
||||||
|
accessToken: apiKey
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
router.push(`/integrations/circleci-context/create?integrationAuthId=${integrationAuth.id}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<Head>
|
||||||
|
<title>Authorize CircleCI Context Integration</title>
|
||||||
|
<link rel="icon" href="/infisical.ico" />
|
||||||
|
</Head>
|
||||||
|
<Card className="mb-12 max-w-lg rounded-md border border-mineshaft-600">
|
||||||
|
<CardTitle
|
||||||
|
className="px-6 text-left text-xl"
|
||||||
|
subTitle="After adding your API Token, 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="flex items-center pb-0.5">
|
||||||
|
<Image
|
||||||
|
src="/images/integrations/Circle CI.png"
|
||||||
|
height={30}
|
||||||
|
width={30}
|
||||||
|
alt="CircleCI logo"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="ml-1.5">CircleCI Context Integration </span>
|
||||||
|
<Link href="https://infisical.com/docs/integrations/cicd/circleci-context" passHref>
|
||||||
|
<a target="_blank" rel="noopener noreferrer">
|
||||||
|
<div className="ml-2 mb-1 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
|
||||||
|
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||||
|
Docs
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faArrowUpRightFromSquare}
|
||||||
|
className="ml-1.5 mb-[0.07rem] text-xxs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</CardTitle>
|
||||||
|
<FormControl
|
||||||
|
label="CircleCI API Token"
|
||||||
|
errorText={apiKeyErrorText}
|
||||||
|
isError={apiKeyErrorText !== "" ?? false}
|
||||||
|
className="px-6"
|
||||||
|
>
|
||||||
|
<Input placeholder="" value={apiKey} onChange={(e) => setApiKey(e.target.value)} />
|
||||||
|
</FormControl>
|
||||||
|
<Button
|
||||||
|
onClick={handleButtonClick}
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
className="mb-6 mt-2 ml-auto mr-6 w-min"
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
Connect to CircleCI
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CircleCIContextCreateIntegrationPage.requireAuth = true;
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardTitle,
|
||||||
|
FilterableSelect,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
Spinner
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { useCreateIntegration } from "@app/hooks/api";
|
||||||
|
import {
|
||||||
|
useGetIntegrationAuthApps,
|
||||||
|
useGetIntegrationAuthCircleCIOrganizations
|
||||||
|
} from "@app/hooks/api/integrationAuth";
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
secretPath: z.string().default("/"),
|
||||||
|
sourceEnvironment: z.object({ name: z.string(), slug: z.string() }),
|
||||||
|
targetOrg: z.object({ name: z.string(), slug: z.string() }),
|
||||||
|
targetContext: z.object({ name: z.string(), appId: z.string() })
|
||||||
|
});
|
||||||
|
|
||||||
|
type TFormData = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
export default function CircleCIContextCreateIntegrationPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const { mutateAsync, isLoading: isCreatingIntegration } = useCreateIntegration();
|
||||||
|
const { currentWorkspace, isLoading: isProjectLoading } = useWorkspace();
|
||||||
|
|
||||||
|
const integrationAuthId = router.query.integrationAuthId as string;
|
||||||
|
|
||||||
|
const { watch, control, reset, handleSubmit } = useForm<TFormData>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
secretPath: "/",
|
||||||
|
sourceEnvironment: currentWorkspace?.environments[0]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const circleCiOrg = watch("targetOrg");
|
||||||
|
|
||||||
|
const { data: circleCIOrganizations, isLoading: isCircleCIOrganizationsLoading } =
|
||||||
|
useGetIntegrationAuthCircleCIOrganizations(integrationAuthId);
|
||||||
|
|
||||||
|
const { data: circleCIContexts } = useGetIntegrationAuthApps(
|
||||||
|
{
|
||||||
|
integrationAuthId,
|
||||||
|
workspaceSlug: circleCiOrg?.slug
|
||||||
|
},
|
||||||
|
|
||||||
|
{ enabled: Boolean(circleCiOrg?.slug) }
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSubmit = async ({
|
||||||
|
sourceEnvironment,
|
||||||
|
secretPath,
|
||||||
|
targetOrg,
|
||||||
|
targetContext
|
||||||
|
}: TFormData) => {
|
||||||
|
try {
|
||||||
|
await mutateAsync({
|
||||||
|
integrationAuthId,
|
||||||
|
isActive: true,
|
||||||
|
sourceEnvironment: sourceEnvironment.slug,
|
||||||
|
app: targetContext.name,
|
||||||
|
appId: targetContext.appId,
|
||||||
|
owner: targetOrg.slug,
|
||||||
|
secretPath
|
||||||
|
});
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
type: "success",
|
||||||
|
text: "Successfully created integration"
|
||||||
|
});
|
||||||
|
router.push(`/integrations/${currentWorkspace?.id}`);
|
||||||
|
} catch (err) {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Failed to create integration"
|
||||||
|
});
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!circleCIContexts || !circleCIOrganizations || !currentWorkspace) return;
|
||||||
|
|
||||||
|
reset({
|
||||||
|
targetOrg: circleCIOrganizations[0],
|
||||||
|
targetContext: circleCIContexts[0]
|
||||||
|
});
|
||||||
|
}, [circleCIOrganizations, circleCIContexts, currentWorkspace]);
|
||||||
|
|
||||||
|
if (isProjectLoading || isCircleCIOrganizationsLoading)
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full items-center justify-center p-24">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
className="flex h-full w-full items-center justify-center"
|
||||||
|
>
|
||||||
|
<Card className="max-w-lg rounded-md p-8 pt-4">
|
||||||
|
<CardTitle
|
||||||
|
className="w-full px-0 text-left text-xl"
|
||||||
|
subTitle="Choose which environment or folder in Infisical you want to sync to CircleCI environment variables."
|
||||||
|
>
|
||||||
|
<div className="flex w-full flex-row items-center justify-between">
|
||||||
|
<div className="flex flex-row items-center gap-1.5">
|
||||||
|
<Image
|
||||||
|
src="/images/integrations/Circle CI.png"
|
||||||
|
height={30}
|
||||||
|
width={30}
|
||||||
|
alt="CircleCI logo"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span className="">CircleCI Context Integration </span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="https://infisical.com/docs/integrations/cicd/circleci-context"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
passHref
|
||||||
|
>
|
||||||
|
<div className="ml-2 mb-1 flex cursor-default cursor-pointer flex-row items-center gap-0.5 rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
|
||||||
|
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||||
|
Docs
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faArrowUpRightFromSquare}
|
||||||
|
className="ml-1.5 mb-[0.07rem] text-xxs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</CardTitle>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="sourceEnvironment"
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
label="Project Environment"
|
||||||
|
>
|
||||||
|
<FilterableSelect
|
||||||
|
getOptionValue={(option) => option.slug}
|
||||||
|
value={value}
|
||||||
|
getOptionLabel={(option) => option.name}
|
||||||
|
onChange={onChange}
|
||||||
|
options={currentWorkspace?.environments}
|
||||||
|
placeholder="Select a project environment"
|
||||||
|
isDisabled={!currentWorkspace?.environments.length}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="secretPath"
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl isError={Boolean(error)} label="Secrets Path">
|
||||||
|
<Input
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
placeholder={'Provide a path (defaults to "/")'}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="targetOrg"
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
label="CircleCI Organization"
|
||||||
|
>
|
||||||
|
<FilterableSelect
|
||||||
|
getOptionValue={(option) => option.slug}
|
||||||
|
value={value}
|
||||||
|
getOptionLabel={(option) => option.name}
|
||||||
|
onChange={onChange}
|
||||||
|
options={circleCIOrganizations}
|
||||||
|
placeholder={
|
||||||
|
circleCIOrganizations?.length
|
||||||
|
? "Select an organization..."
|
||||||
|
: "No organizations found..."
|
||||||
|
}
|
||||||
|
isDisabled={!circleCIOrganizations?.length}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="targetContext"
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl errorText={error?.message} isError={Boolean(error)} label="Bitbucket Repo">
|
||||||
|
<FilterableSelect
|
||||||
|
value={value}
|
||||||
|
getOptionValue={(option) => option.appId!}
|
||||||
|
getOptionLabel={(option) => option.name}
|
||||||
|
onChange={onChange}
|
||||||
|
options={circleCIContexts}
|
||||||
|
placeholder={
|
||||||
|
circleCIContexts?.length ? "Select a context..." : "No contexts found..."
|
||||||
|
}
|
||||||
|
isDisabled={!circleCIContexts?.length}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
colorSchema="primary"
|
||||||
|
className="mt-4"
|
||||||
|
isLoading={isCreatingIntegration}
|
||||||
|
isDisabled={isCreatingIntegration || !circleCIContexts?.length}
|
||||||
|
>
|
||||||
|
Create Integration
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CircleCIContextCreateIntegrationPage.requireAuth = true;
|
||||||
+11
-1
@@ -46,6 +46,8 @@ export const IntegrationConnectionSection = ({ integration }: Props) => {
|
|||||||
case "qovery":
|
case "qovery":
|
||||||
return integration.scope;
|
return integration.scope;
|
||||||
case "circleci":
|
case "circleci":
|
||||||
|
case "circleci-context":
|
||||||
|
return "Context";
|
||||||
case "terraform-cloud":
|
case "terraform-cloud":
|
||||||
return "Project";
|
return "Project";
|
||||||
case "aws-secret-manager":
|
case "aws-secret-manager":
|
||||||
@@ -77,7 +79,6 @@ export const IntegrationConnectionSection = ({ integration }: Props) => {
|
|||||||
return `${integration.owner}`;
|
return `${integration.owner}`;
|
||||||
}
|
}
|
||||||
return `${integration.owner}/${integration.app}`;
|
return `${integration.owner}/${integration.app}`;
|
||||||
|
|
||||||
case "aws-parameter-store":
|
case "aws-parameter-store":
|
||||||
case "rundeck":
|
case "rundeck":
|
||||||
return `${integration.path}`;
|
return `${integration.path}`;
|
||||||
@@ -155,6 +156,15 @@ export const IntegrationConnectionSection = ({ integration }: Props) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (integration.integration === "circleci-context" && integration.owner) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<FormLabel className="text-sm font-semibold text-mineshaft-300" label="Organization" />
|
||||||
|
<div className="text-sm text-mineshaft-300">{integration.owner}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (integration.integration === "terraform-cloud" && integration.targetService) {
|
if (integration.integration === "terraform-cloud" && integration.targetService) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -120,6 +120,9 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) =>
|
|||||||
case "circleci":
|
case "circleci":
|
||||||
link = `${window.location.origin}/integrations/circleci/authorize`;
|
link = `${window.location.origin}/integrations/circleci/authorize`;
|
||||||
break;
|
break;
|
||||||
|
case "circleci-context":
|
||||||
|
link = `${window.location.origin}/integrations/circleci-context/authorize`;
|
||||||
|
break;
|
||||||
case "databricks":
|
case "databricks":
|
||||||
link = `${window.location.origin}/integrations/databricks/authorize`;
|
link = `${window.location.origin}/integrations/databricks/authorize`;
|
||||||
break;
|
break;
|
||||||
|
|||||||
+8
@@ -15,6 +15,7 @@ export const getIntegrationDestination = (integration: TIntegration) =>
|
|||||||
(["aws-parameter-store", "rundeck"].includes(integration.integration) && `${integration.path}`) ||
|
(["aws-parameter-store", "rundeck"].includes(integration.integration) && `${integration.path}`) ||
|
||||||
(integration.scope?.startsWith("github-") && `${integration.owner}/${integration.app}`) ||
|
(integration.scope?.startsWith("github-") && `${integration.owner}/${integration.app}`) ||
|
||||||
integration.app ||
|
integration.app ||
|
||||||
|
(integration.integration === "circleci-context" && `${integration.owner}`) ||
|
||||||
"-";
|
"-";
|
||||||
|
|
||||||
export const IntegrationDetails = ({ integration }: Props) => {
|
export const IntegrationDetails = ({ integration }: Props) => {
|
||||||
@@ -53,6 +54,7 @@ export const IntegrationDetails = ({ integration }: Props) => {
|
|||||||
label={
|
label={
|
||||||
(integration.integration === "qovery" && integration?.scope) ||
|
(integration.integration === "qovery" && integration?.scope) ||
|
||||||
(integration.integration === "circleci" && "Project") ||
|
(integration.integration === "circleci" && "Project") ||
|
||||||
|
(integration.integration === "circleci-context" && "Context") ||
|
||||||
(integration.integration === "bitbucket" && "Repository") ||
|
(integration.integration === "bitbucket" && "Repository") ||
|
||||||
(integration.integration === "octopus-deploy" && "Project") ||
|
(integration.integration === "octopus-deploy" && "Project") ||
|
||||||
(integration.integration === "aws-secret-manager" && "Secret") ||
|
(integration.integration === "aws-secret-manager" && "Secret") ||
|
||||||
@@ -110,6 +112,12 @@ export const IntegrationDetails = ({ integration }: Props) => {
|
|||||||
<div className={FIELD_CLASSNAME}>{integration.owner}</div>
|
<div className={FIELD_CLASSNAME}>{integration.owner}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{integration.integration === "circleci-context" && integration.owner && (
|
||||||
|
<div>
|
||||||
|
<FormLabel label="Organization Slug" />
|
||||||
|
<div className={FIELD_CLASSNAME}>{integration.owner}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{integration.integration === "terraform-cloud" && integration.targetService && (
|
{integration.integration === "terraform-cloud" && integration.targetService && (
|
||||||
<div>
|
<div>
|
||||||
<FormLabel label="Category" />
|
<FormLabel label="Category" />
|
||||||
|
|||||||
Reference in New Issue
Block a user