General improvements to Humanitec integration

This commit is contained in:
carlosmonastyrski
2025-03-13 09:41:12 -03:00
parent 568aadef75
commit b0b55344ce
27 changed files with 340 additions and 166 deletions
@@ -136,8 +136,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
return "Service Account Impersonation";
case DatabricksConnectionMethod.ServicePrincipal:
return "Service Principal";
case HumanitecConnectionMethod.AccessKey:
return "Access Key";
case HumanitecConnectionMethod.API_TOKEN:
return "API Token";
default:
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Unhandled App Connection Method: ${method}`);
@@ -1,3 +1,3 @@
export enum HumanitecConnectionMethod {
AccessKey = "access-key"
API_TOKEN = "api-token"
}
@@ -2,6 +2,7 @@ import { AxiosError, AxiosResponse } from "axios";
import { request } from "@app/lib/config/request";
import { BadRequestError, InternalServerError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
@@ -18,7 +19,7 @@ export const getHumanitecConnectionListItem = () => {
return {
name: "Humanitec" as const,
app: AppConnection.Humanitec as const,
methods: Object.values(HumanitecConnectionMethod) as [HumanitecConnectionMethod.AccessKey]
methods: Object.values(HumanitecConnectionMethod) as [HumanitecConnectionMethod.API_TOKEN]
};
};
@@ -30,13 +31,13 @@ export const validateHumanitecConnectionCredentials = async (config: THumanitecC
try {
response = await request.get<HumanitecOrg[]>(`${IntegrationUrls.HUMANITEC_API_URL}/orgs`, {
headers: {
Authorization: `Bearer ${inputCredentials.accessKeyId}`
Authorization: `Bearer ${inputCredentials.apiToken}`
}
});
} catch (error: unknown) {
if (error instanceof AxiosError) {
throw new BadRequestError({
message: `Failed to validate credentials: ${error.response?.data || "Unknown error"}`
message: `Failed to validate credentials: ${error.message || "Unknown error"}`
});
}
throw new BadRequestError({
@@ -55,11 +56,11 @@ export const validateHumanitecConnectionCredentials = async (config: THumanitecC
export const listOrganizations = async (appConnection: THumanitecConnection): Promise<HumanitecOrgWithApps[]> => {
const {
credentials: { accessKeyId }
credentials: { apiToken }
} = appConnection;
const response = await request.get<HumanitecOrg[]>(`${IntegrationUrls.HUMANITEC_API_URL}/orgs`, {
headers: {
Authorization: `Bearer ${accessKeyId}`
Authorization: `Bearer ${apiToken}`
}
});
@@ -68,34 +69,39 @@ export const listOrganizations = async (appConnection: THumanitecConnection): Pr
message: "Failed to get organizations: Response was empty"
});
}
const orgs = response.data;
const appPromises = orgs.map(async (org) => {
return request.get<HumanitecApp[]>(`${IntegrationUrls.HUMANITEC_API_URL}/orgs/${org.id}/apps`, {
headers: {
Authorization: `Bearer ${accessKeyId}`
const orgsWithApps: HumanitecOrgWithApps[] = [];
for (const org of orgs) {
try {
// eslint-disable-next-line no-await-in-loop
const appsResponse = await request.get<HumanitecApp[]>(
`${IntegrationUrls.HUMANITEC_API_URL}/orgs/${org.id}/apps`,
{
headers: {
Authorization: `Bearer ${apiToken}`
}
}
);
if (!appsResponse.data) {
throw new InternalServerError({
message: "Failed to get apps for organization: Response was empty"
});
}
});
});
const appsResponses = await Promise.all(appPromises);
const orgsWithApps: HumanitecOrgWithApps[] = orgs.map((org, index) => {
if (!appsResponses[index].data) {
throw new InternalServerError({
message: "Failed to get apps for organization: Response was empty"
const apps = appsResponse.data;
orgsWithApps.push({
...org,
apps: apps.map((app) => ({
name: app.name,
id: app.id,
envs: app.envs
}))
});
} catch (error) {
logger.error(error, `Failed to get apps for organization ${org.name}`);
}
const apps = appsResponses[index].data;
return {
...org,
apps: apps.map((app) => ({
name: app.name,
id: app.id,
envs: app.envs
}))
};
});
}
return orgsWithApps;
};
@@ -11,19 +11,19 @@ import {
import { HumanitecConnectionMethod } from "./humanitec-connection-enums";
export const HumanitecConnectionAccessTokenCredentialsSchema = z.object({
accessKeyId: z.string().trim().min(1, "Access Key ID required")
apiToken: z.string().trim().min(1, "API Token required")
});
const BaseHumanitecConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Humanitec) });
export const HumanitecConnectionSchema = BaseHumanitecConnectionSchema.extend({
method: z.literal(HumanitecConnectionMethod.AccessKey),
method: z.literal(HumanitecConnectionMethod.API_TOKEN),
credentials: HumanitecConnectionAccessTokenCredentialsSchema
});
export const SanitizedHumanitecConnectionSchema = z.discriminatedUnion("method", [
BaseHumanitecConnectionSchema.extend({
method: z.literal(HumanitecConnectionMethod.AccessKey),
method: z.literal(HumanitecConnectionMethod.API_TOKEN),
credentials: HumanitecConnectionAccessTokenCredentialsSchema.pick({})
})
]);
@@ -31,7 +31,7 @@ export const SanitizedHumanitecConnectionSchema = z.discriminatedUnion("method",
export const ValidateHumanitecConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z
.literal(HumanitecConnectionMethod.AccessKey)
.literal(HumanitecConnectionMethod.API_TOKEN)
.describe(AppConnections?.CREATE(AppConnection.Humanitec).method),
credentials: HumanitecConnectionAccessTokenCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.Humanitec).credentials
@@ -1,3 +1,4 @@
import { logger } from "@app/lib/logger";
import { OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
@@ -17,6 +18,7 @@ export const humanitecConnectionService = (getAppConnection: TGetAppConnectionFu
const organizations = await getHumanitecOrganizations(appConnection);
return organizations;
} catch (error) {
logger.error(error, "Failed to establish connection with Humanitec");
return [];
}
};