mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 14:27:30 +00:00
misc: unified to a single integration
This commit is contained in:
@@ -1202,7 +1202,19 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
|
|||||||
organizations: z
|
organizations: z
|
||||||
.object({
|
.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
slug: z.string()
|
slug: z.string(),
|
||||||
|
projects: z
|
||||||
|
.object({
|
||||||
|
name: z.string(),
|
||||||
|
id: z.string()
|
||||||
|
})
|
||||||
|
.array(),
|
||||||
|
contexts: z
|
||||||
|
.object({
|
||||||
|
name: z.string(),
|
||||||
|
id: z.string()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ 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";
|
||||||
|
|
||||||
@@ -490,47 +489,6 @@ 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
|
||||||
*/
|
*/
|
||||||
@@ -1237,12 +1195,6 @@ 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,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable no-await-in-loop */
|
||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
import { createAppAuth } from "@octokit/auth-app";
|
import { createAppAuth } from "@octokit/auth-app";
|
||||||
import { Octokit } from "@octokit/rest";
|
import { Octokit } from "@octokit/rest";
|
||||||
@@ -11,6 +12,7 @@ import { getConfig } from "@app/lib/config/env";
|
|||||||
import { request } from "@app/lib/config/request";
|
import { request } from "@app/lib/config/request";
|
||||||
import { decryptSymmetric128BitHexKeyUTF8, encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
import { decryptSymmetric128BitHexKeyUTF8, encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
||||||
import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
import { TGenericPermission, TProjectPermission } from "@app/lib/types";
|
import { TGenericPermission, TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
import { TIntegrationDALFactory } from "../integration/integration-dal";
|
import { TIntegrationDALFactory } from "../integration/integration-dal";
|
||||||
@@ -18,6 +20,7 @@ import { TKmsServiceFactory } from "../kms/kms-service";
|
|||||||
import { KmsDataKey } from "../kms/kms-types";
|
import { KmsDataKey } from "../kms/kms-types";
|
||||||
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
|
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
|
||||||
import { getApps } from "./integration-app-list";
|
import { getApps } from "./integration-app-list";
|
||||||
|
import { TCircleCIContext } from "./integration-app-types";
|
||||||
import { TIntegrationAuthDALFactory } from "./integration-auth-dal";
|
import { TIntegrationAuthDALFactory } from "./integration-auth-dal";
|
||||||
import { IntegrationAuthMetadataSchema, TIntegrationAuthMetadata } from "./integration-auth-schema";
|
import { IntegrationAuthMetadataSchema, TIntegrationAuthMetadata } from "./integration-auth-schema";
|
||||||
import {
|
import {
|
||||||
@@ -1593,8 +1596,8 @@ export const integrationAuthServiceFactory = ({
|
|||||||
const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId);
|
const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId);
|
||||||
const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
|
const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
|
||||||
|
|
||||||
const { data }: { data: TCircleCIOrganization[] } = await request.get(
|
const { data: organizations }: { data: TCircleCIOrganization[] } = await request.get(
|
||||||
`${IntegrationUrls.CIRCLECI_CONTEXT_API_URL}/v2/me/collaborations`,
|
`${IntegrationUrls.CIRCLECI_API_URL}/v2/me/collaborations`,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
"Circle-Token": `${accessToken}`,
|
"Circle-Token": `${accessToken}`,
|
||||||
@@ -1603,7 +1606,106 @@ export const integrationAuthServiceFactory = ({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return data;
|
let projects: {
|
||||||
|
orgName: string;
|
||||||
|
projectName: string;
|
||||||
|
projectId?: string;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const projectRes = (
|
||||||
|
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;
|
||||||
|
|
||||||
|
projects = projectRes.map((a) => ({
|
||||||
|
orgName: a.username, // username maps to unique organization name in CircleCI
|
||||||
|
projectName: a.reponame, // reponame maps to project name within an organization in CircleCI
|
||||||
|
projectId: a.vcs_url.split("/").pop() // vcs_url maps to the project id in CircleCI
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectsByOrg = projects.reduce<Record<string, { name: string; id: string }[]>>((accum, project) => {
|
||||||
|
if (!accum[project.orgName]) {
|
||||||
|
return {
|
||||||
|
...accum,
|
||||||
|
[project.orgName]: [
|
||||||
|
{
|
||||||
|
name: project.projectName,
|
||||||
|
id: project.projectId as string
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...accum,
|
||||||
|
[project.orgName]: [
|
||||||
|
...accum[project.orgName],
|
||||||
|
{
|
||||||
|
name: project.projectName,
|
||||||
|
id: project.projectId as string
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const getOrgContexts = async (orgSlug: string) => {
|
||||||
|
type NextPageToken = string | null | undefined;
|
||||||
|
|
||||||
|
type CircleCIContextResponse = {
|
||||||
|
items: TCircleCIContext[];
|
||||||
|
next_page_token: NextPageToken;
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const contexts: TCircleCIContext[] = [];
|
||||||
|
let nextPageToken: NextPageToken;
|
||||||
|
|
||||||
|
while (nextPageToken !== null) {
|
||||||
|
const res = (
|
||||||
|
await request.get<CircleCIContextResponse>(`${IntegrationUrls.CIRCLECI_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;
|
||||||
|
}
|
||||||
|
|
||||||
|
return contexts?.map((context) => ({
|
||||||
|
name: context.name,
|
||||||
|
id: context.id
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return Promise.all(
|
||||||
|
organizations.map(async (org) => ({
|
||||||
|
name: org.name,
|
||||||
|
slug: org.slug,
|
||||||
|
projects: projectsByOrg[org.name] ?? [],
|
||||||
|
contexts: (await getOrgContexts(org.slug)) ?? []
|
||||||
|
}))
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteIntegrationAuths = async ({
|
const deleteIntegrationAuths = async ({
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ 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",
|
||||||
@@ -78,7 +77,6 @@ export enum IntegrationUrls {
|
|||||||
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
|
// 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",
|
||||||
@@ -229,15 +227,6 @@ export const getIntegrationOptions = async () => {
|
|||||||
clientId: "",
|
clientId: "",
|
||||||
docsLink: ""
|
docsLink: ""
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "CircleCI Contexts",
|
|
||||||
slug: "circleci-context",
|
|
||||||
image: "CircleCI.png",
|
|
||||||
isAvailable: true,
|
|
||||||
type: "pat",
|
|
||||||
clientId: "",
|
|
||||||
docsLink: ""
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "Databricks",
|
name: "Databricks",
|
||||||
slug: "databricks",
|
slug: "databricks",
|
||||||
|
|||||||
@@ -2245,6 +2245,87 @@ const syncSecretsCircleCI = async ({
|
|||||||
secrets: Record<string, { value: string; comment?: string }>;
|
secrets: Record<string, { value: string; comment?: string }>;
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
}) => {
|
}) => {
|
||||||
|
enum CircleCiScope {
|
||||||
|
Project = "project",
|
||||||
|
Context = "context"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (integration.scope === CircleCiScope.Context) {
|
||||||
|
// 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"
|
||||||
|
},
|
||||||
|
params: nextPageToken
|
||||||
|
? new URLSearchParams({
|
||||||
|
"page-token": nextPageToken
|
||||||
|
})
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
const getProjectSlug = async () => {
|
const getProjectSlug = async () => {
|
||||||
const requestConfig = {
|
const requestConfig = {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -2341,89 +2422,7 @@ 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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -4516,13 +4515,6 @@ 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,7 +16,6 @@ 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",
|
||||||
|
|||||||
@@ -108,6 +108,14 @@ export enum OctopusDeployScope {
|
|||||||
export type CircleCIOrganization = {
|
export type CircleCIOrganization = {
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
|
projects: {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
}[];
|
||||||
|
contexts: {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TGetIntegrationAuthOctopusDeployScopeValuesDTO = {
|
export type TGetIntegrationAuthOctopusDeployScopeValuesDTO = {
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
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 justify-between">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Image
|
|
||||||
src="/images/integrations/CircleCI.png"
|
|
||||||
height={30}
|
|
||||||
width={30}
|
|
||||||
alt="CircleCI logo"
|
|
||||||
/>
|
|
||||||
<span className="ml-1.5">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-pointer flex-row items-center gap-1.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} />
|
|
||||||
Docs
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faArrowUpRightFromSquare}
|
|
||||||
className="mb-[0.07rem] text-xxs"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</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;
|
|
||||||
@@ -1,249 +0,0 @@
|
|||||||
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 Context 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/CircleCI.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-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="CircleCI Context"
|
|
||||||
>
|
|
||||||
<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;
|
|
||||||
@@ -1,155 +1,148 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import Head from "next/head";
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import {
|
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||||
faArrowUpRightFromSquare,
|
|
||||||
faBookOpen,
|
|
||||||
faBugs,
|
|
||||||
faCircleInfo
|
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import queryString from "query-string";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { useCreateIntegration } from "@app/hooks/api";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
|
FilterableSelect,
|
||||||
FormControl,
|
FormControl,
|
||||||
Input,
|
|
||||||
Select,
|
Select,
|
||||||
SelectItem
|
SelectItem,
|
||||||
} from "../../../components/v2";
|
Spinner
|
||||||
import {
|
} from "@app/components/v2";
|
||||||
useGetIntegrationAuthApps,
|
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
|
||||||
useGetIntegrationAuthById
|
import { useWorkspace } from "@app/context";
|
||||||
} from "../../../hooks/api/integrationAuth";
|
import { useCreateIntegration } from "@app/hooks/api";
|
||||||
import { useGetWorkspaceById } from "../../../hooks/api/workspace";
|
import { useGetIntegrationAuthCircleCIOrganizations } from "@app/hooks/api/integrationAuth";
|
||||||
|
|
||||||
|
const formSchema = z.discriminatedUnion("scope", [
|
||||||
|
z.object({
|
||||||
|
scope: z.literal("context"),
|
||||||
|
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(), id: z.string() })
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
scope: z.literal("project"),
|
||||||
|
secretPath: z.string().default("/"),
|
||||||
|
sourceEnvironment: z.object({ name: z.string(), slug: z.string() }),
|
||||||
|
targetOrg: z.object({ name: z.string(), slug: z.string() }),
|
||||||
|
targetProject: z.object({ name: z.string(), id: z.string() })
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
type TFormData = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
export default function CircleCICreateIntegrationPage() {
|
export default function CircleCICreateIntegrationPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { mutateAsync } = useCreateIntegration();
|
const { mutateAsync, isLoading: isCreatingIntegration } = useCreateIntegration();
|
||||||
|
const { currentWorkspace, isLoading: isProjectLoading } = useWorkspace();
|
||||||
|
|
||||||
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]);
|
const integrationAuthId = router.query.integrationAuthId as string;
|
||||||
|
|
||||||
const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? "");
|
const { control, watch, handleSubmit } = useForm<TFormData>({
|
||||||
const { data: integrationAuth, isLoading: isintegrationAuthLoading } = useGetIntegrationAuthById(
|
resolver: zodResolver(formSchema),
|
||||||
(integrationAuthId as string) ?? ""
|
defaultValues: {
|
||||||
);
|
secretPath: "/",
|
||||||
const { data: integrationAuthApps, isLoading: isIntegrationAuthAppsLoading } =
|
sourceEnvironment: currentWorkspace?.environments[0]
|
||||||
useGetIntegrationAuthApps({
|
}
|
||||||
integrationAuthId: (integrationAuthId as string) ?? ""
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
|
const selectedScope = watch("scope");
|
||||||
const [targetOrganization, setTargetOrganization] = useState("");
|
const selectedOrg = watch("targetOrg");
|
||||||
const [secretPath, setSecretPath] = useState("/");
|
|
||||||
|
|
||||||
const [targetProjectId, setTargetProjectId] = useState("");
|
const { data: circleCIOrganizations, isLoading: isCircleCIOrganizationsLoading } =
|
||||||
|
useGetIntegrationAuthCircleCIOrganizations(integrationAuthId);
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const selectedOrganizationEntry = selectedOrg
|
||||||
|
? circleCIOrganizations?.find((org) => org.slug === selectedOrg.slug)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
useEffect(() => {
|
const onSubmit = async (data: TFormData) => {
|
||||||
if (workspace) {
|
|
||||||
setSelectedSourceEnvironment(workspace.environments[0].slug);
|
|
||||||
}
|
|
||||||
}, [workspace]);
|
|
||||||
|
|
||||||
const handleButtonClick = async () => {
|
|
||||||
try {
|
try {
|
||||||
if (!integrationAuth?.id) return;
|
if (data.scope === "context") {
|
||||||
|
|
||||||
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({
|
await mutateAsync({
|
||||||
integrationAuthId: integrationAuth?.id,
|
scope: data.scope,
|
||||||
|
integrationAuthId,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
app: selectedApp.name, // project name
|
sourceEnvironment: data.sourceEnvironment.slug,
|
||||||
owner: selectedApp.owner, // organization name
|
app: data.targetContext.name,
|
||||||
appId: selectedApp.appId, // project id (used for syncing)
|
appId: data.targetContext.id,
|
||||||
sourceEnvironment: selectedSourceEnvironment,
|
owner: data.targetOrg.name,
|
||||||
secretPath
|
secretPath: data.secretPath
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
await mutateAsync({
|
||||||
|
scope: data.scope,
|
||||||
|
integrationAuthId,
|
||||||
|
isActive: true,
|
||||||
|
app: data.targetProject.name, // project name
|
||||||
|
owner: data.targetOrg.name, // organization name
|
||||||
|
appId: data.targetProject.id, // project id (used for syncing)
|
||||||
|
sourceEnvironment: data.sourceEnvironment.slug,
|
||||||
|
secretPath: data.secretPath
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
setIsLoading(false);
|
createNotification({
|
||||||
|
type: "success",
|
||||||
router.push(`/integrations/${localStorage.getItem("projectData.id")}`);
|
text: "Successfully created integration"
|
||||||
|
});
|
||||||
|
router.push(`/integrations/${currentWorkspace?.id}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Failed to create integration"
|
||||||
|
});
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredProjects = useMemo(() => {
|
if (isProjectLoading || isCircleCIOrganizationsLoading)
|
||||||
if (!integrationAuthApps) return [];
|
return (
|
||||||
|
<div className="flex h-full w-full items-center justify-center p-24">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return integrationAuthApps.filter((integrationAuthApp) => {
|
return (
|
||||||
return integrationAuthApp.owner === targetOrganization;
|
<form
|
||||||
});
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
}, [integrationAuthApps, targetOrganization]);
|
className="flex h-full w-full items-center justify-center"
|
||||||
|
|
||||||
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>
|
|
||||||
<link rel="icon" href="/infisical.ico" />
|
|
||||||
</Head>
|
|
||||||
<Card className="max-w-lg rounded-md border border-mineshaft-600">
|
|
||||||
<CardTitle
|
|
||||||
className="px-6 text-left text-xl"
|
|
||||||
subTitle="Choose which environment or folder in Infisical you want to sync to CircleCI environment variables."
|
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center">
|
<Card className="max-w-lg rounded-md p-8 pt-4">
|
||||||
<div className="flex items-center pb-0.5">
|
<CardTitle
|
||||||
|
className="w-full px-0 text-left text-xl"
|
||||||
|
subTitle="Choose which environment or folder in Infisical you want to sync to CircleCI."
|
||||||
|
>
|
||||||
|
<div className="flex w-full flex-row items-center justify-between">
|
||||||
|
<div className="flex flex-row items-center gap-1.5">
|
||||||
<Image
|
<Image
|
||||||
src="/images/integrations/CircleCI.png"
|
src="/images/integrations/CircleCI.png"
|
||||||
height={30}
|
height={30}
|
||||||
width={30}
|
width={30}
|
||||||
alt="CircleCI logo"
|
alt="CircleCI logo"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<span className="">CircleCI Context Integration </span>
|
||||||
</div>
|
</div>
|
||||||
<span className="ml-1.5">CircleCI Integration </span>
|
|
||||||
<Link href="https://infisical.com/docs/integrations/cicd/circleci" passHref>
|
<Link
|
||||||
<a target="_blank" rel="noopener noreferrer">
|
href="https://infisical.com/docs/integrations/cicd/circleci"
|
||||||
<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">
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
passHref
|
||||||
|
>
|
||||||
|
<div className="ml-2 mb-1 flex 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" />
|
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||||
Docs
|
Docs
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
@@ -157,137 +150,147 @@ export default function CircleCICreateIntegrationPage() {
|
|||||||
className="ml-1.5 mb-[0.07rem] text-xxs"
|
className="ml-1.5 mb-[0.07rem] text-xxs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
|
<Controller
|
||||||
<FormControl label="Project Environment" className="px-6">
|
control={control}
|
||||||
<Select
|
name="sourceEnvironment"
|
||||||
value={selectedSourceEnvironment}
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
<FormControl
|
||||||
className="w-full border border-mineshaft-500"
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
label="Project Environment"
|
||||||
>
|
>
|
||||||
{workspace?.environments.map((sourceEnvironment) => (
|
<FilterableSelect
|
||||||
<SelectItem
|
getOptionValue={(option) => option.slug}
|
||||||
value={sourceEnvironment.slug}
|
value={value}
|
||||||
key={`source-environment-${sourceEnvironment.slug}`}
|
getOptionLabel={(option) => option.name}
|
||||||
>
|
onChange={onChange}
|
||||||
{sourceEnvironment.name}
|
options={currentWorkspace?.environments}
|
||||||
</SelectItem>
|
placeholder="Select a project environment"
|
||||||
))}
|
isDisabled={!currentWorkspace?.environments.length}
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
<FormControl label="Secrets Path" className="px-6">
|
|
||||||
<Input
|
|
||||||
value={secretPath}
|
|
||||||
onChange={(evt) => setSecretPath(evt.target.value)}
|
|
||||||
placeholder="Provide a path, default is /"
|
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
)}
|
||||||
<FormControl label="CircleCI Organization" className="px-6">
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="secretPath"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Secrets Path" errorText={error?.message} isError={Boolean(error)}>
|
||||||
|
<SecretPathInput {...field} />
|
||||||
|
</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="scope"
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Scope" errorText={error?.message} isError={Boolean(error)}>
|
||||||
<Select
|
<Select
|
||||||
value={targetOrganization}
|
defaultValue={field.value}
|
||||||
onValueChange={(val) => {
|
onValueChange={(e) => {
|
||||||
setTargetOrganization(val);
|
onChange(e);
|
||||||
setTargetProjectId("none");
|
|
||||||
}}
|
}}
|
||||||
className="w-full border border-mineshaft-500"
|
className="w-full border border-mineshaft-500"
|
||||||
isDisabled={filteredOrganizations.length === 0}
|
|
||||||
>
|
>
|
||||||
{filteredOrganizations.length > 0 ? (
|
<SelectItem value="project">Project</SelectItem>
|
||||||
filteredOrganizations.map((org) => (
|
<SelectItem value="context">Context</SelectItem>
|
||||||
<SelectItem value={org} key={`target-org-${org}`}>
|
|
||||||
{org}
|
|
||||||
</SelectItem>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<SelectItem value="none" key="target-app-none">
|
|
||||||
No organizations found
|
|
||||||
</SelectItem>
|
|
||||||
)}
|
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</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>
|
/>
|
||||||
|
{selectedScope === "context" && selectedOrganizationEntry && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="targetContext"
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
label="CircleCI Context"
|
||||||
|
>
|
||||||
|
<FilterableSelect
|
||||||
|
value={value}
|
||||||
|
getOptionValue={(option) => option.id!}
|
||||||
|
getOptionLabel={(option) => option.name}
|
||||||
|
onChange={onChange}
|
||||||
|
options={selectedOrganizationEntry?.contexts}
|
||||||
|
placeholder={
|
||||||
|
selectedOrganizationEntry.contexts?.length
|
||||||
|
? "Select a context..."
|
||||||
|
: "No contexts found..."
|
||||||
|
}
|
||||||
|
isDisabled={!selectedOrganizationEntry.contexts?.length}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{selectedScope === "project" && selectedOrganizationEntry && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="targetProject"
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
label="CircleCI Project"
|
||||||
|
>
|
||||||
|
<FilterableSelect
|
||||||
|
value={value}
|
||||||
|
getOptionValue={(option) => option.id!}
|
||||||
|
getOptionLabel={(option) => option.name}
|
||||||
|
onChange={onChange}
|
||||||
|
options={selectedOrganizationEntry?.projects}
|
||||||
|
placeholder={
|
||||||
|
selectedOrganizationEntry.projects?.length
|
||||||
|
? "Select a project..."
|
||||||
|
: "No projects found..."
|
||||||
|
}
|
||||||
|
isDisabled={!selectedOrganizationEntry.projects?.length}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
onClick={handleButtonClick}
|
type="submit"
|
||||||
colorSchema="primary"
|
colorSchema="primary"
|
||||||
variant="outline_bg"
|
className="mt-4"
|
||||||
className="mb-6 mt-2 ml-auto mr-6 w-min"
|
isLoading={isCreatingIntegration}
|
||||||
isLoading={isLoading}
|
isDisabled={isCreatingIntegration}
|
||||||
isDisabled={integrationAuthApps.length === 0}
|
|
||||||
>
|
>
|
||||||
Create Integration
|
Create Integration
|
||||||
</Button>
|
</Button>
|
||||||
</Card>
|
</Card>
|
||||||
<div className="mt-6 w-full max-w-md border-t border-mineshaft-800" />
|
</form>
|
||||||
<div className="mt-6 flex w-full max-w-lg flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4">
|
|
||||||
<div className="flex flex-row items-center">
|
|
||||||
<FontAwesomeIcon icon={faCircleInfo} className="text-xl text-mineshaft-200" />{" "}
|
|
||||||
<span className="text-md ml-3 text-mineshaft-100">Pro Tip</span>
|
|
||||||
</div>
|
|
||||||
<span className="mt-4 text-sm text-mineshaft-300">
|
|
||||||
After creating an integration, your secrets will start syncing immediately. This might
|
|
||||||
cause an unexpected override of current secrets in CircleCI with secrets from Infisical.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
|
||||||
<Head>
|
|
||||||
<title>Set Up CircleCI Integration</title>
|
|
||||||
<link rel="icon" href="/infisical.ico" />
|
|
||||||
</Head>
|
|
||||||
{isIntegrationAuthAppsLoading || isintegrationAuthLoading ? (
|
|
||||||
<img
|
|
||||||
src="/images/loading/loading.gif"
|
|
||||||
height={70}
|
|
||||||
width={120}
|
|
||||||
alt="infisical loading indicator"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="flex h-max max-w-md flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6 text-center text-mineshaft-200">
|
|
||||||
<FontAwesomeIcon icon={faBugs} className="inlineli my-2 text-6xl" />
|
|
||||||
<p>
|
|
||||||
Something went wrong. Please contact{" "}
|
|
||||||
<a
|
|
||||||
className="inline cursor-pointer text-mineshaft-100 underline decoration-primary-500 underline-offset-4 opacity-80 duration-200 hover:opacity-100"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
href="mailto:[email protected]"
|
|
||||||
>
|
|
||||||
support@infisical.com
|
|
||||||
</a>{" "}
|
|
||||||
if the issue persists.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-10
@@ -46,8 +46,11 @@ export const IntegrationConnectionSection = ({ integration }: Props) => {
|
|||||||
case "qovery":
|
case "qovery":
|
||||||
return integration.scope;
|
return integration.scope;
|
||||||
case "circleci":
|
case "circleci":
|
||||||
case "circleci-context":
|
if (integration.scope === "context") {
|
||||||
return "Context";
|
return "Context";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Project";
|
||||||
case "terraform-cloud":
|
case "terraform-cloud":
|
||||||
return "Project";
|
return "Project";
|
||||||
case "aws-secret-manager":
|
case "aws-secret-manager":
|
||||||
@@ -156,15 +159,6 @@ 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,9 +120,6 @@ 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;
|
||||||
|
|||||||
+2
-9
@@ -15,7 +15,6 @@ 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,8 +52,8 @@ export const IntegrationDetails = ({ integration }: Props) => {
|
|||||||
<FormLabel
|
<FormLabel
|
||||||
label={
|
label={
|
||||||
(integration.integration === "qovery" && integration?.scope) ||
|
(integration.integration === "qovery" && integration?.scope) ||
|
||||||
(integration.integration === "circleci" && "Project") ||
|
(integration.integration === "circleci" &&
|
||||||
(integration.integration === "circleci-context" && "Context") ||
|
(integration.scope === "context" ? "Context" : "Project")) ||
|
||||||
(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") ||
|
||||||
@@ -112,12 +111,6 @@ 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