Merge pull request #1419 from Infisical/integration-update-heroku

Integration update heroku
This commit is contained in:
BlackMagiq
2024-03-09 19:15:00 -08:00
committed by GitHub
35 changed files with 2027 additions and 289 deletions
@@ -109,7 +109,7 @@ const getAppsGCPSecretManager = async ({ accessToken }: { accessToken: string })
*/
const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => {
const res = (
await request.get<{ name: string }[]>(`${IntegrationUrls.HEROKU_API_URL}/apps`, {
await request.get<{ name: string; id: string }[]>(`${IntegrationUrls.HEROKU_API_URL}/apps`, {
headers: {
Accept: "application/vnd.heroku+json; version=3",
Authorization: `Bearer ${accessToken}`
@@ -118,7 +118,8 @@ const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => {
).data;
const apps = res.map((a) => ({
name: a.name
name: a.name,
appId: a.id
}));
return apps;
@@ -20,9 +20,11 @@ import {
TDeleteIntegrationAuthsDTO,
TGetIntegrationAuthDTO,
TGetIntegrationAuthTeamCityBuildConfigDTO,
THerokuPipelineCoupling,
TIntegrationAuthAppsDTO,
TIntegrationAuthBitbucketWorkspaceDTO,
TIntegrationAuthChecklyGroupsDTO,
TIntegrationAuthHerokuPipelinesDTO,
TIntegrationAuthNorthflankSecretGroupDTO,
TIntegrationAuthQoveryEnvironmentsDTO,
TIntegrationAuthQoveryOrgsDTO,
@@ -576,6 +578,38 @@ export const integrationAuthServiceFactory = ({
return [];
};
const getHerokuPipelines = async ({ id, actor, actorId, actorOrgId }: TIntegrationAuthHerokuPipelinesDTO) => {
const integrationAuth = await integrationAuthDAL.findById(id);
if (!integrationAuth) throw new BadRequestError({ message: "Failed to find integration" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
integrationAuth.projectId,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
const botKey = await projectBotService.getBotKey(integrationAuth.projectId);
const { accessToken } = await getIntegrationAccessToken(integrationAuth, botKey);
const { data } = await request.get<THerokuPipelineCoupling[]>(
`${IntegrationUrls.HEROKU_API_URL}/pipeline-couplings`,
{
headers: {
Accept: "application/vnd.heroku+json; version=3",
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
}
);
return data.map(({ app: { id: appId }, stage, pipeline: { id: pipelineId, name } }) => ({
app: { appId },
stage,
pipeline: { pipelineId, name }
}));
};
const getRailwayEnvironments = async ({ id, actor, actorId, actorOrgId, appId }: TIntegrationAuthRailwayEnvDTO) => {
const integrationAuth = await integrationAuthDAL.findById(id);
if (!integrationAuth) throw new BadRequestError({ message: "Failed to find integration" });
@@ -904,6 +938,7 @@ export const integrationAuthServiceFactory = ({
getQoveryApps,
getQoveryEnvs,
getQoveryJobs,
getHerokuPipelines,
getQoveryOrgs,
getQoveryProjects,
getQoveryContainers,
@@ -62,6 +62,10 @@ export type TIntegrationAuthQoveryScopesDTO = {
environmentId: string;
} & Omit<TProjectPermission, "projectId">;
export type TIntegrationAuthHerokuPipelinesDTO = {
id: string;
} & Omit<TProjectPermission, "projectId">;
export type TIntegrationAuthRailwayEnvDTO = {
id: string;
appId: string;
@@ -129,6 +133,12 @@ export type TNorthflankSecretGroup = {
projectId: string;
};
export type THerokuPipelineCoupling = {
app: { id: string };
stage: string;
pipeline: { id: string; name: string };
};
export type TTeamCityBuildConfig = {
id: string;
name: string;
@@ -37,6 +37,12 @@ export enum IntegrationType {
OAUTH2 = "oauth2"
}
export enum IntegrationInitialSyncBehavior {
OVERWRITE_TARGET = "overwrite-target",
PREFER_TARGET = "prefer-target",
PREFER_SOURCE = "prefer-source"
}
export enum IntegrationUrls {
// integration oauth endpoints
GCP_TOKEN_URL = "https://oauth2.googleapis.com/token",
@@ -20,11 +20,13 @@ import sodium from "libsodium-wrappers";
import isEqual from "lodash.isequal";
import { z } from "zod";
import { TIntegrationAuths, TIntegrations } from "@app/db/schemas";
import { SecretType, TIntegrationAuths, TIntegrations, TSecrets } from "@app/db/schemas";
import { request } from "@app/lib/config/request";
import { BadRequestError } from "@app/lib/errors";
import { TCreateManySecretsRawFn, TUpdateManySecretsRawFn } from "@app/services/secret/secret-types";
import { Integrations, IntegrationUrls } from "./integration-list";
import { TIntegrationDALFactory } from "../integration/integration-dal";
import { IntegrationInitialSyncBehavior, Integrations, IntegrationUrls } from "./integration-list";
const getSecretKeyValuePair = (secrets: Record<string, { value: string | null; comment?: string } | null>) =>
Object.keys(secrets).reduce<Record<string, string | null | undefined>>((prev, key) => {
@@ -582,11 +584,25 @@ const syncSecretsAWSSecretManager = async ({
* Sync/push [secrets] to Heroku app named [integration.app]
*/
const syncSecretsHeroku = async ({
createManySecretsRawFn,
updateManySecretsRawFn,
integrationDAL,
integration,
secrets,
accessToken
}: {
integration: TIntegrations;
createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise<Array<TSecrets & { _id: string }>>;
updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise<Array<TSecrets & { _id: string }>>;
integrationDAL: Pick<TIntegrationDALFactory, "updateById">;
integration: TIntegrations & {
projectId: string;
environment: {
id: string;
name: string;
slug: string;
};
secretPath: string;
};
secrets: Record<string, { value: string; comment?: string } | null>;
accessToken: string;
}) => {
@@ -600,12 +616,74 @@ const syncSecretsHeroku = async ({
})
).data;
const secretsToAdd: { [key: string]: string } = {};
const secretsToUpdate: { [key: string]: string } = {};
const metadata = z.record(z.any()).parse(integration.metadata);
Object.keys(herokuSecrets).forEach((key) => {
if (!(key in secrets)) {
secrets[key] = null;
}
if (!integration.lastUsed) {
// first time using integration
// -> apply initial sync behavior
switch (metadata.initialSyncBehavior) {
case IntegrationInitialSyncBehavior.OVERWRITE_TARGET: {
if (!(key in secrets)) secrets[key] = null;
break;
}
case IntegrationInitialSyncBehavior.PREFER_TARGET: {
if (!(key in secrets)) {
secretsToAdd[key] = herokuSecrets[key];
} else if (secrets[key]?.value !== herokuSecrets[key]) {
secretsToUpdate[key] = herokuSecrets[key];
}
secrets[key] = {
value: herokuSecrets[key]
};
break;
}
case IntegrationInitialSyncBehavior.PREFER_SOURCE: {
if (!(key in secrets)) {
secrets[key] = herokuSecrets[key];
secretsToAdd[key] = herokuSecrets[key];
}
break;
}
default: {
if (!(key in secrets)) secrets[key] = null;
break;
}
}
} else if (!(key in secrets)) secrets[key] = null;
});
if (Object.keys(secretsToAdd).length) {
await createManySecretsRawFn({
projectId: integration.projectId,
environment: integration.environment.slug,
path: integration.secretPath,
secrets: Object.keys(secretsToAdd).map((key) => ({
secretName: key,
secretValue: secretsToAdd[key],
type: SecretType.Shared,
secretComment: ""
}))
});
}
if (Object.keys(secretsToUpdate).length) {
await updateManySecretsRawFn({
projectId: integration.projectId,
environment: integration.environment.slug,
path: integration.secretPath,
secrets: Object.keys(secretsToUpdate).map((key) => ({
secretName: key,
secretValue: secretsToUpdate[key],
type: SecretType.Shared,
secretComment: ""
}))
});
}
await request.patch(
`${IntegrationUrls.HEROKU_API_URL}/apps/${integration.app}/config-vars`,
getSecretKeyValuePair(secrets),
@@ -617,6 +695,10 @@ const syncSecretsHeroku = async ({
}
}
);
await integrationDAL.updateById(integration.id, {
lastUsed: new Date()
});
};
/**
@@ -2930,8 +3012,14 @@ const syncSecretsHasuraCloud = async ({
/**
* Sync/push [secrets] to [app] in integration named [integration]
*
* Do this in terms of DAL
*
*/
export const syncIntegrationSecrets = async ({
createManySecretsRawFn,
updateManySecretsRawFn,
integrationDAL,
integration,
integrationAuth,
secrets,
@@ -2939,7 +3027,18 @@ export const syncIntegrationSecrets = async ({
accessToken,
appendices
}: {
integration: TIntegrations;
createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise<Array<TSecrets & { _id: string }>>;
updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise<Array<TSecrets & { _id: string }>>;
integrationDAL: Pick<TIntegrationDALFactory, "updateById">;
integration: TIntegrations & {
projectId: string;
environment: {
id: string;
name: string;
slug: string;
};
secretPath: string;
};
integrationAuth: TIntegrationAuths;
secrets: Record<string, { value: string; comment?: string }>;
accessId: string | null;
@@ -2979,6 +3078,9 @@ export const syncIntegrationSecrets = async ({
break;
case Integrations.HEROKU:
await syncSecretsHeroku({
createManySecretsRawFn,
updateManySecretsRawFn,
integrationDAL,
integration,
secrets,
accessToken