Skip to content

Commit

Permalink
Merge pull request #2883 from Infisical/feat/sync-circle-ci-context
Browse files Browse the repository at this point in the history
feat: circle ci context integration
  • Loading branch information
sheensantoscapadngan authored Dec 16, 2024
2 parents 9cd0dc8 + 9e76864 commit b669b0a
Show file tree
Hide file tree
Showing 19 changed files with 659 additions and 325 deletions.
46 changes: 46 additions & 0 deletions backend/src/server/routes/v1/integration-auth-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1185,4 +1185,50 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
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(),
projects: z
.object({
name: z.string(),
id: z.string()
})
.array(),
contexts: z
.object({
name: z.string(),
id: z.string()
})
.array()
})
.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 };
}
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type TCircleCIContext = {
id: string;
name: string;
created_at: string;
};
120 changes: 120 additions & 0 deletions backend/src/services/integration-auth/integration-auth-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,24 @@ import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
import { decryptSymmetric128BitHexKeyUTF8, encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
import { logger } from "@app/lib/logger";
import { TGenericPermission, TProjectPermission } from "@app/lib/types";

import { TIntegrationDALFactory } from "../integration/integration-dal";
import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types";
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
import { getApps } from "./integration-app-list";
import { TCircleCIContext } from "./integration-app-types";
import { TIntegrationAuthDALFactory } from "./integration-auth-dal";
import { IntegrationAuthMetadataSchema, TIntegrationAuthMetadata } from "./integration-auth-schema";
import {
OctopusDeployScope,
TBitbucketEnvironment,
TBitbucketWorkspace,
TChecklyGroups,
TCircleCIOrganization,
TDeleteIntegrationAuthByIdDTO,
TDeleteIntegrationAuthsDTO,
TDuplicateGithubIntegrationAuthDTO,
Expand All @@ -42,6 +46,7 @@ import {
TIntegrationAuthBitbucketEnvironmentsDTO,
TIntegrationAuthBitbucketWorkspaceDTO,
TIntegrationAuthChecklyGroupsDTO,
TIntegrationAuthCircleCIOrganizationDTO,
TIntegrationAuthGithubEnvsDTO,
TIntegrationAuthGithubOrgsDTO,
TIntegrationAuthHerokuPipelinesDTO,
Expand Down Expand Up @@ -1578,6 +1583,120 @@ export const integrationAuthServiceFactory = ({
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: organizations }: { data: TCircleCIOrganization[] } = await request.get(
`${IntegrationUrls.CIRCLECI_API_URL}/v2/me/collaborations`,
{
headers: {
"Circle-Token": `${accessToken}`,
"Accept-Encoding": "application/json"
}
}
);

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 = groupBy(
projects.map((p) => ({
orgName: p.orgName,
name: p.projectName,
id: p.projectId as string
})),
(p) => p.orgName
);

const getOrgContexts = async (orgSlug: string) => {
type NextPageToken = string | null | undefined;

try {
const contexts: TCircleCIContext[] = [];
let nextPageToken: NextPageToken;

while (nextPageToken !== null) {
// eslint-disable-next-line no-await-in-loop
const { data } = await request.get<{
items: TCircleCIContext[];
next_page_token: NextPageToken;
}>(`${IntegrationUrls.CIRCLECI_API_URL}/v2/context`, {
headers: {
"Circle-Token": accessToken,
"Accept-Encoding": "application/json"
},
params: new URLSearchParams({
"owner-slug": orgSlug,
...(nextPageToken ? { "page-token": nextPageToken } : {})
})
});

contexts.push(...data.items);
nextPageToken = data.next_page_token;
}

return contexts?.map((context) => ({
name: context.name,
id: context.id
}));
} catch (error) {
logger.error(error);
}
};

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 ({
projectId,
integration,
Expand Down Expand Up @@ -1790,6 +1909,7 @@ export const integrationAuthServiceFactory = ({
getTeamcityBuildConfigs,
getBitbucketWorkspaces,
getBitbucketEnvironments,
getCircleCIOrganizations,
getIntegrationAccessToken,
duplicateIntegrationAuth,
getOctopusDeploySpaces,
Expand Down
17 changes: 17 additions & 0 deletions backend/src/services/integration-auth/integration-auth-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,10 @@ export type TGetIntegrationAuthTeamCityBuildConfigDTO = {
appId: string;
} & Omit<TProjectPermission, "projectId">;

export type TIntegrationAuthCircleCIOrganizationDTO = {
id: string;
} & Omit<TProjectPermission, "projectId">;

export type TVercelBranches = {
ref: string;
lastCommit: string;
Expand Down Expand Up @@ -189,6 +193,14 @@ export type TTeamCityBuildConfig = {
webUrl: string;
};

export type TCircleCIOrganization = {
id: string;
vcsType: string;
name: string;
avatarUrl: string;
slug: string;
};

export type TIntegrationsWithEnvironment = TIntegrations & {
environment?:
| {
Expand All @@ -215,6 +227,11 @@ export enum OctopusDeployScope {
// add tenant, variable set, etc.
}

export enum CircleCiScope {
Project = "project",
Context = "context"
}

export type TOctopusDeployVariableSet = {
Id: string;
OwnerId: string;
Expand Down
5 changes: 2 additions & 3 deletions backend/src/services/integration-auth/integration-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ export enum IntegrationUrls {
RAILWAY_API_URL = "https://backboard.railway.app/graphql/v2",
FLYIO_API_URL = "https://api.fly.io/graphql",
CIRCLECI_API_URL = "https://circleci.com/api",
DATABRICKS_API_URL = "https:/xxxx.com/api",
TRAVISCI_API_URL = "https://api.travis-ci.com",
SUPABASE_API_URL = "https://api.supabase.com",
LARAVELFORGE_API_URL = "https://forge.laravel.com",
Expand Down Expand Up @@ -218,9 +217,9 @@ export const getIntegrationOptions = async () => {
docsLink: ""
},
{
name: "Circle CI",
name: "CircleCI",
slug: "circleci",
image: "Circle CI.png",
image: "CircleCI.png",
isAvailable: true,
type: "pat",
clientId: "",
Expand Down
Loading

0 comments on commit b669b0a

Please sign in to comment.