Initial Commit for terraform cloud intergation

This commit is contained in:
carlosmonastyrski
2025-04-03 11:15:38 -03:00
parent d6da108e32
commit f00a54ed54
54 changed files with 1519 additions and 28 deletions
@@ -5,7 +5,8 @@ export enum AppConnection {
GCP = "gcp",
AzureKeyVault = "azure-key-vault",
AzureAppConfiguration = "azure-app-configuration",
Humanitec = "humanitec"
Humanitec = "humanitec",
TerraformCloud = "terraform-cloud"
}
export enum AWSRegion {
@@ -40,6 +40,11 @@ import {
HumanitecConnectionMethod,
validateHumanitecConnectionCredentials
} from "./humanitec";
import {
getTerraformCloudConnectionListItem,
TerraformCloudConnectionMethod,
validateTerraformCloudConnectionCredentials
} from "./terraform-cloud";
export const listAppConnectionOptions = () => {
return [
@@ -49,7 +54,8 @@ export const listAppConnectionOptions = () => {
getAzureKeyVaultConnectionListItem(),
getAzureAppConfigurationConnectionListItem(),
getDatabricksConnectionListItem(),
getHumanitecConnectionListItem()
getHumanitecConnectionListItem(),
getTerraformCloudConnectionListItem()
].sort((a, b) => a.name.localeCompare(b.name));
};
@@ -114,6 +120,8 @@ export const validateAppConnectionCredentials = async (
return validateAzureAppConfigurationConnectionCredentials(appConnection);
case AppConnection.Humanitec:
return validateHumanitecConnectionCredentials(appConnection);
case AppConnection.TerraformCloud:
return validateTerraformCloudConnectionCredentials(appConnection);
default:
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Unhandled App Connection ${app}`);
@@ -138,6 +146,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
return "Service Principal";
case HumanitecConnectionMethod.API_TOKEN:
return "API Token";
case TerraformCloudConnectionMethod.API_TOKEN:
return "API Token";
default:
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Unhandled App Connection Method: ${method}`);
@@ -7,5 +7,6 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.AzureKeyVault]: "Azure Key Vault",
[AppConnection.AzureAppConfiguration]: "Azure App Configuration",
[AppConnection.Databricks]: "Databricks",
[AppConnection.Humanitec]: "Humanitec"
[AppConnection.Humanitec]: "Humanitec",
[AppConnection.TerraformCloud]: "Terraform Cloud"
};
@@ -37,6 +37,8 @@ import { ValidateGitHubConnectionCredentialsSchema } from "./github";
import { githubConnectionService } from "./github/github-connection-service";
import { ValidateHumanitecConnectionCredentialsSchema } from "./humanitec";
import { humanitecConnectionService } from "./humanitec/humanitec-connection-service";
import { ValidateTerraformCloudConnectionCredentialsSchema } from "./terraform-cloud";
import { terraformCloudConnectionService } from "./terraform-cloud/terraform-cloud-connection-service";
export type TAppConnectionServiceFactoryDep = {
appConnectionDAL: TAppConnectionDALFactory;
@@ -53,7 +55,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.AzureKeyVault]: ValidateAzureKeyVaultConnectionCredentialsSchema,
[AppConnection.AzureAppConfiguration]: ValidateAzureAppConfigurationConnectionCredentialsSchema,
[AppConnection.Databricks]: ValidateDatabricksConnectionCredentialsSchema,
[AppConnection.Humanitec]: ValidateHumanitecConnectionCredentialsSchema
[AppConnection.Humanitec]: ValidateHumanitecConnectionCredentialsSchema,
[AppConnection.TerraformCloud]: ValidateTerraformCloudConnectionCredentialsSchema
};
export const appConnectionServiceFactory = ({
@@ -375,6 +378,7 @@ export const appConnectionServiceFactory = ({
gcp: gcpConnectionService(connectAppConnectionById),
databricks: databricksConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
aws: awsConnectionService(connectAppConnectionById),
humanitec: humanitecConnectionService(connectAppConnectionById)
humanitec: humanitecConnectionService(connectAppConnectionById),
terraformCloud: terraformCloudConnectionService(connectAppConnectionById)
};
};
@@ -0,0 +1,4 @@
export * from "./terraform-cloud-connection-enums";
export * from "./terraform-cloud-connection-fns";
export * from "./terraform-cloud-connection-schemas";
export * from "./terraform-cloud-connection-types";
@@ -0,0 +1,3 @@
export enum TerraformCloudConnectionMethod {
API_TOKEN = "api-token"
}
@@ -0,0 +1,143 @@
import { AxiosError, AxiosResponse } from "axios";
import { request } from "@app/lib/config/request";
import { BadRequestError, InternalServerError } from "@app/lib/errors";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { TerraformCloudConnectionMethod } from "./terraform-cloud-connection-enums";
import {
TTerraformCloudConnection,
TTerraformCloudConnectionConfig,
TTerraformCloudOrganization,
TTerraformCloudProject,
TTerraformCloudWorkspace
} from "./terraform-cloud-connection-types";
export const getTerraformCloudConnectionListItem = () => {
return {
name: "Terraform Cloud" as const,
app: AppConnection.TerraformCloud as const,
methods: Object.values(TerraformCloudConnectionMethod) as [TerraformCloudConnectionMethod.API_TOKEN]
};
};
export const validateTerraformCloudConnectionCredentials = async (config: TTerraformCloudConnectionConfig) => {
const { credentials: inputCredentials } = config;
let response: AxiosResponse<{ data: TTerraformCloudOrganization[] }> | null = null;
try {
response = await request.get<{ data: TTerraformCloudOrganization[] }>(
`${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/organizations`,
{
headers: {
Authorization: `Bearer ${inputCredentials.apiToken}`,
"Content-Type": "application/vnd.api+json"
}
}
);
} catch (error: unknown) {
if (error instanceof AxiosError) {
throw new BadRequestError({
message: `Failed to validate credentials: ${error.message || "Unknown error"}`
});
}
throw new BadRequestError({
message: "Unable to validate connection - verify credentials"
});
}
if (!response?.data) {
throw new InternalServerError({
message: "Failed to get organizations: Response was empty"
});
}
return inputCredentials;
};
export const listOrganizations = async (
appConnection: TTerraformCloudConnection
): Promise<TTerraformCloudOrganization[]> => {
const {
credentials: { apiToken }
} = appConnection;
const orgsResponse = await request.get<{ data: { id: string; attributes: { name: string } }[] }>(
`${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/organizations`,
{
headers: {
Authorization: `Bearer ${apiToken}`,
"Content-Type": "application/vnd.api+json"
}
}
);
if (!orgsResponse.data?.data) {
throw new InternalServerError({
message: "Failed to get organizations: Response was empty"
});
}
const orgEntities = orgsResponse.data.data;
const orgsWithProjectsAndWorkspaces: TTerraformCloudOrganization[] = [];
const projectPromises = orgEntities.map((org) =>
request
.get<{ data: { id: string; attributes: { name: string } }[] }>(
`${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/organizations/${org.id}/projects`,
{
headers: {
Authorization: `Bearer ${apiToken}`,
"Content-Type": "application/vnd.api+json"
}
}
)
.catch(() => ({ data: { data: [] } }))
);
const workspacePromises = orgEntities.map((org) =>
request
.get<{ data: { id: string; attributes: { name: string } }[] }>(
`${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/organizations/${org.id}/workspaces`,
{
headers: {
Authorization: `Bearer ${apiToken}`,
"Content-Type": "application/vnd.api+json"
}
}
)
.catch(() => ({ data: { data: [] } }))
);
const [projectResponses, workspaceResponses] = await Promise.all([
Promise.all(projectPromises),
Promise.all(workspacePromises)
]);
for (let i = 0; i < orgEntities.length; i += 1) {
const org = orgEntities[i];
const projectsData = projectResponses[i].data?.data || [];
const workspacesData = workspaceResponses[i].data?.data || [];
const projects: TTerraformCloudProject[] = projectsData.map((project) => ({
id: project.id,
name: project.attributes.name
}));
const workspaces: TTerraformCloudWorkspace[] = workspacesData.map((workspace) => ({
id: workspace.id,
name: workspace.attributes.name
}));
orgsWithProjectsAndWorkspaces.push({
id: org.id,
name: org.attributes.name,
projects,
workspaces
});
}
return orgsWithProjectsAndWorkspaces;
};
@@ -0,0 +1,60 @@
import z from "zod";
import { AppConnections } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
BaseAppConnectionSchema,
GenericCreateAppConnectionFieldsSchema,
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { TerraformCloudConnectionMethod } from "./terraform-cloud-connection-enums";
export const TerraformCloudConnectionAccessTokenCredentialsSchema = z.object({
apiToken: z.string().trim().min(1, "API Token required")
});
const BaseTerraformCloudConnectionSchema = BaseAppConnectionSchema.extend({
app: z.literal(AppConnection.TerraformCloud)
});
export const TerraformCloudConnectionSchema = BaseTerraformCloudConnectionSchema.extend({
method: z.literal(TerraformCloudConnectionMethod.API_TOKEN),
credentials: TerraformCloudConnectionAccessTokenCredentialsSchema
});
export const SanitizedTerraformCloudConnectionSchema = z.discriminatedUnion("method", [
BaseTerraformCloudConnectionSchema.extend({
method: z.literal(TerraformCloudConnectionMethod.API_TOKEN),
credentials: TerraformCloudConnectionAccessTokenCredentialsSchema.pick({})
})
]);
export const ValidateTerraformCloudConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z
.literal(TerraformCloudConnectionMethod.API_TOKEN)
.describe(AppConnections?.CREATE(AppConnection.TerraformCloud).method),
credentials: TerraformCloudConnectionAccessTokenCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.TerraformCloud).credentials
)
})
]);
export const CreateTerraformCloudConnectionSchema = ValidateTerraformCloudConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.TerraformCloud)
);
export const UpdateTerraformCloudConnectionSchema = z
.object({
credentials: TerraformCloudConnectionAccessTokenCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.TerraformCloud).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.TerraformCloud));
export const TerraformCloudConnectionListItemSchema = z.object({
name: z.literal("Terraform Cloud"),
app: z.literal(AppConnection.TerraformCloud),
methods: z.nativeEnum(TerraformCloudConnectionMethod).array()
});
@@ -0,0 +1,29 @@
import { logger } from "@app/lib/logger";
import { OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import { listOrganizations as getTerraformCloudOrganizations } from "./terraform-cloud-connection-fns";
import { TTerraformCloudConnection } from "./terraform-cloud-connection-types";
type TGetAppConnectionFunc = (
app: AppConnection,
connectionId: string,
actor: OrgServiceActor
) => Promise<TTerraformCloudConnection>;
export const terraformCloudConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
const listOrganizations = async (connectionId: string, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.TerraformCloud, connectionId, actor);
try {
const organizations = await getTerraformCloudOrganizations(appConnection);
return organizations;
} catch (error) {
logger.error(error, "Failed to establish connection with Terraform Cloud");
return [];
}
};
return {
listOrganizations
};
};
@@ -0,0 +1,66 @@
import z from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import {
CreateTerraformCloudConnectionSchema,
TerraformCloudConnectionSchema,
ValidateTerraformCloudConnectionCredentialsSchema
} from "./terraform-cloud-connection-schemas";
export type TTerraformCloudConnection = z.infer<typeof TerraformCloudConnectionSchema>;
export type TTerraformCloudConnectionInput = z.infer<typeof CreateTerraformCloudConnectionSchema> & {
app: AppConnection.TerraformCloud;
};
export type TValidateTerraformCloudConnectionCredentials = typeof ValidateTerraformCloudConnectionCredentialsSchema;
export type TTerraformCloudConnectionConfig = DiscriminativePick<
TTerraformCloudConnectionInput,
"method" | "app" | "credentials"
> & {
orgId: string;
};
export type TerraformCloudOrg = {
id: string;
name: string;
};
export type TerraformCloudApp = {
name: string;
id: string;
envs: { name: string; id: string }[];
};
export type TerraformCloudOrgWithApps = TerraformCloudOrg & {
apps: TerraformCloudApp[];
};
export type TTerraformCloudProject = {
id: string;
name: string;
};
export type TTerraformCloudWorkspace = {
id: string;
name: string;
};
export type TTerraformCloudOrganization = {
id: string;
name: string;
projects: TTerraformCloudProject[];
workspaces: TTerraformCloudWorkspace[];
};
export type TTerraformCloudConnectionOrganization = TTerraformCloudOrganization;
export type TTerraformCloudConnectionProject = TTerraformCloudProject;
export type TTerraformCloudConnectionWorkspace = TTerraformCloudWorkspace;
export enum TerraformCloudSyncScope {
Project = "project",
Workspace = "workspace"
}