diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 066314228..8e390d9a3 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1769,6 +1769,9 @@ export const AppConnections = { sslRejectUnauthorized: "Whether or not to reject unauthorized SSL certificates.", sslCertificate: "The SSL certificate to use for connection." }, + TERRAFORM_CLOUD: { + apiToken: "The API token to use to connect with Terraform Cloud." + }, VERCEL: { apiToken: "The API token used to authenticate with Vercel." }, @@ -1895,6 +1898,15 @@ export const SecretSyncs = { env: "The ID of the Humanitec environment to sync secrets to.", scope: "The Humanitec scope that secrets should be synced to." }, + TERRAFORM_CLOUD: { + org: "The ID of the Terraform Cloud org to sync secrets to.", + variableSetName: "The name of the Terraform Cloud Variable Set to sync secrets to.", + variableSetId: "The ID of the Terraform Cloud Variable Set to sync secrets to.", + workspaceName: "The name of the Terraform Cloud workspace to sync secrets to.", + workspaceId: "The ID of the Terraform Cloud workspace to sync secrets to.", + scope: "The Terraform Cloud scope that secrets should be synced to.", + category: "The Terraform Cloud category that secrets should be synced to." + }, VERCEL: { app: "The ID of the Vercel app to sync secrets to.", appName: "The name of the Vercel app to sync secrets to.", diff --git a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts index 040d32bf4..ebc23a4bd 100644 --- a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts @@ -31,6 +31,10 @@ import { PostgresConnectionListItemSchema, SanitizedPostgresConnectionSchema } from "@app/services/app-connection/postgres"; +import { + SanitizedTerraformCloudConnectionSchema, + TerraformCloudConnectionListItemSchema +} from "@app/services/app-connection/terraform-cloud"; import { SanitizedVercelConnectionSchema, VercelConnectionListItemSchema } from "@app/services/app-connection/vercel"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -43,6 +47,7 @@ const SanitizedAppConnectionSchema = z.union([ ...SanitizedAzureAppConfigurationConnectionSchema.options, ...SanitizedDatabricksConnectionSchema.options, ...SanitizedHumanitecConnectionSchema.options, + ...SanitizedTerraformCloudConnectionSchema.options, ...SanitizedVercelConnectionSchema.options, ...SanitizedPostgresConnectionSchema.options, ...SanitizedMsSqlConnectionSchema.options, @@ -57,6 +62,7 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ AzureAppConfigurationConnectionListItemSchema, DatabricksConnectionListItemSchema, HumanitecConnectionListItemSchema, + TerraformCloudConnectionListItemSchema, VercelConnectionListItemSchema, PostgresConnectionListItemSchema, MsSqlConnectionListItemSchema, diff --git a/backend/src/server/routes/v1/app-connection-routers/index.ts b/backend/src/server/routes/v1/app-connection-routers/index.ts index aa5297800..b89eb0991 100644 --- a/backend/src/server/routes/v1/app-connection-routers/index.ts +++ b/backend/src/server/routes/v1/app-connection-routers/index.ts @@ -10,6 +10,7 @@ import { registerGitHubConnectionRouter } from "./github-connection-router"; import { registerHumanitecConnectionRouter } from "./humanitec-connection-router"; import { registerMsSqlConnectionRouter } from "./mssql-connection-router"; import { registerPostgresConnectionRouter } from "./postgres-connection-router"; +import { registerTerraformCloudConnectionRouter } from "./terraform-cloud-router"; import { registerVercelConnectionRouter } from "./vercel-connection-router"; export * from "./app-connection-router"; @@ -23,6 +24,7 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record { + registerAppConnectionEndpoints({ + app: AppConnection.TerraformCloud, + server, + sanitizedResponseSchema: SanitizedTerraformCloudConnectionSchema, + createSchema: CreateTerraformCloudConnectionSchema, + updateSchema: UpdateTerraformCloudConnectionSchema + }); + + // The below endpoints are not exposed and for Infisical App use + server.route({ + method: "GET", + url: `/:connectionId/organizations`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + response: { + 200: z + .object({ + id: z.string(), + name: z.string(), + variableSets: z + .object({ + id: z.string(), + name: z.string(), + description: z.string().optional(), + global: z.boolean().optional() + }) + .array(), + workspaces: z + .object({ + id: z.string(), + name: z.string() + }) + .array() + }) + .array() + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { connectionId } = req.params; + + const organizations: TTerraformCloudOrganization[] = + await server.services.appConnection.terraformCloud.listOrganizations(connectionId, req.permission); + + return organizations; + } + }); +}; diff --git a/backend/src/server/routes/v1/secret-sync-routers/index.ts b/backend/src/server/routes/v1/secret-sync-routers/index.ts index ad087fdd1..07567124d 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/index.ts +++ b/backend/src/server/routes/v1/secret-sync-routers/index.ts @@ -9,6 +9,7 @@ import { registerDatabricksSyncRouter } from "./databricks-sync-router"; import { registerGcpSyncRouter } from "./gcp-sync-router"; import { registerGitHubSyncRouter } from "./github-sync-router"; import { registerHumanitecSyncRouter } from "./humanitec-sync-router"; +import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router"; import { registerVercelSyncRouter } from "./vercel-sync-router"; export * from "./secret-sync-router"; @@ -22,6 +23,7 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record + registerSyncSecretsEndpoints({ + destination: SecretSync.TerraformCloud, + server, + responseSchema: TerraformCloudSyncSchema, + createSchema: CreateTerraformCloudSyncSchema, + updateSchema: UpdateTerraformCloudSyncSchema + }); diff --git a/backend/src/services/app-connection/app-connection-enums.ts b/backend/src/services/app-connection/app-connection-enums.ts index df77cf02e..f5eb31598 100644 --- a/backend/src/services/app-connection/app-connection-enums.ts +++ b/backend/src/services/app-connection/app-connection-enums.ts @@ -6,6 +6,7 @@ export enum AppConnection { AzureKeyVault = "azure-key-vault", AzureAppConfiguration = "azure-app-configuration", Humanitec = "humanitec", + TerraformCloud = "terraform-cloud", Vercel = "vercel", Postgres = "postgres", MsSql = "mssql", diff --git a/backend/src/services/app-connection/app-connection-fns.ts b/backend/src/services/app-connection/app-connection-fns.ts index 696f3d4a3..2c54a1fbc 100644 --- a/backend/src/services/app-connection/app-connection-fns.ts +++ b/backend/src/services/app-connection/app-connection-fns.ts @@ -42,6 +42,11 @@ import { } from "./humanitec"; import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql"; import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres"; +import { + getTerraformCloudConnectionListItem, + TerraformCloudConnectionMethod, + validateTerraformCloudConnectionCredentials +} from "./terraform-cloud"; import { VercelConnectionMethod } from "./vercel"; import { getVercelConnectionListItem, validateVercelConnectionCredentials } from "./vercel/vercel-connection-fns"; @@ -54,6 +59,7 @@ export const listAppConnectionOptions = () => { getAzureAppConfigurationConnectionListItem(), getDatabricksConnectionListItem(), getHumanitecConnectionListItem(), + getTerraformCloudConnectionListItem(), getVercelConnectionListItem(), getPostgresConnectionListItem(), getMsSqlConnectionListItem(), @@ -114,6 +120,7 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record case CamundaConnectionMethod.ClientCredentials: return "Client Credentials"; case HumanitecConnectionMethod.ApiToken: + case TerraformCloudConnectionMethod.ApiToken: case VercelConnectionMethod.ApiToken: return "API Token"; case PostgresConnectionMethod.UsernameAndPassword: @@ -186,6 +194,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record< [AppConnection.Humanitec]: platformManagedCredentialsNotSupported, [AppConnection.Postgres]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform, [AppConnection.MsSql]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform, + [AppConnection.TerraformCloud]: platformManagedCredentialsNotSupported, [AppConnection.Camunda]: platformManagedCredentialsNotSupported, [AppConnection.Vercel]: platformManagedCredentialsNotSupported }; diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts index 9df7037c2..8c77bfc58 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -8,6 +8,7 @@ export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.AzureAppConfiguration]: "Azure App Configuration", [AppConnection.Databricks]: "Databricks", [AppConnection.Humanitec]: "Humanitec", + [AppConnection.TerraformCloud]: "Terraform Cloud", [AppConnection.Vercel]: "Vercel", [AppConnection.Postgres]: "PostgreSQL", [AppConnection.MsSql]: "Microsoft SQL Server", diff --git a/backend/src/services/app-connection/app-connection-service.ts b/backend/src/services/app-connection/app-connection-service.ts index 3114b4731..143cce3e3 100644 --- a/backend/src/services/app-connection/app-connection-service.ts +++ b/backend/src/services/app-connection/app-connection-service.ts @@ -43,6 +43,8 @@ import { ValidateHumanitecConnectionCredentialsSchema } from "./humanitec"; import { humanitecConnectionService } from "./humanitec/humanitec-connection-service"; import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql"; import { ValidatePostgresConnectionCredentialsSchema } from "./postgres"; +import { ValidateTerraformCloudConnectionCredentialsSchema } from "./terraform-cloud"; +import { terraformCloudConnectionService } from "./terraform-cloud/terraform-cloud-connection-service"; import { ValidateVercelConnectionCredentialsSchema } from "./vercel"; import { vercelConnectionService } from "./vercel/vercel-connection-service"; @@ -62,6 +64,7 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record { + return { + name: "Terraform Cloud" as const, + app: AppConnection.TerraformCloud as const, + methods: Object.values(TerraformCloudConnectionMethod) as [TerraformCloudConnectionMethod.ApiToken] + }; +}; + +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 => { + const { + credentials: { apiToken } + } = appConnection; + + const headers = { + Authorization: `Bearer ${apiToken}`, + "Content-Type": "application/vnd.api+json" + }; + + const fetchAllPages = async (url: string): Promise => { + let results: T[] = []; + let nextUrl: string | null = url; + + while (nextUrl) { + // eslint-disable-next-line no-await-in-loop + const res: AxiosResponse<{ data: T[]; links?: { next?: string } }> = await request.get(nextUrl, { headers }); + results = results.concat(res.data.data); + nextUrl = res.data.links?.next || null; + } + + return results; + }; + + const orgEntities = await fetchAllPages<{ id: string; attributes: { name: string } }>( + `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/organizations` + ); + + const orgsWithVariableSetsAndWorkspaces: TTerraformCloudOrganization[] = []; + + const variableSetPromises = orgEntities.map((org) => + fetchAllPages<{ id: string; attributes: { name: string; description?: string; global?: boolean } }>( + `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/organizations/${org.id}/varsets` + ).catch(() => []) + ); + + const workspacePromises = orgEntities.map((org) => + fetchAllPages<{ id: string; attributes: { name: string } }>( + `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/organizations/${org.id}/workspaces` + ).catch(() => []) + ); + + const [variableSetResults, workspaceResults] = await Promise.all([ + Promise.all(variableSetPromises), + Promise.all(workspacePromises) + ]); + + for (let i = 0; i < orgEntities.length; i += 1) { + const org = orgEntities[i]; + const variableSetsData = variableSetResults[i]; + const workspacesData = workspaceResults[i]; + + const variableSets: TTerraformCloudVariableSet[] = variableSetsData.map((varSet) => ({ + id: varSet.id, + name: varSet.attributes.name, + description: varSet.attributes.description, + global: varSet.attributes.global + })); + + const workspaces: TTerraformCloudWorkspace[] = workspacesData.map((workspace) => ({ + id: workspace.id, + name: workspace.attributes.name + })); + + orgsWithVariableSetsAndWorkspaces.push({ + id: org.id, + name: org.attributes.name, + variableSets, + workspaces + }); + } + + return orgsWithVariableSetsAndWorkspaces; +}; diff --git a/backend/src/services/app-connection/terraform-cloud/terraform-cloud-connection-schemas.ts b/backend/src/services/app-connection/terraform-cloud/terraform-cloud-connection-schemas.ts new file mode 100644 index 000000000..0d408ba4f --- /dev/null +++ b/backend/src/services/app-connection/terraform-cloud/terraform-cloud-connection-schemas.ts @@ -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").describe(AppConnections.CREDENTIALS.TERRAFORM_CLOUD.apiToken) +}); + +const BaseTerraformCloudConnectionSchema = BaseAppConnectionSchema.extend({ + app: z.literal(AppConnection.TerraformCloud) +}); + +export const TerraformCloudConnectionSchema = BaseTerraformCloudConnectionSchema.extend({ + method: z.literal(TerraformCloudConnectionMethod.ApiToken), + credentials: TerraformCloudConnectionAccessTokenCredentialsSchema +}); + +export const SanitizedTerraformCloudConnectionSchema = z.discriminatedUnion("method", [ + BaseTerraformCloudConnectionSchema.extend({ + method: z.literal(TerraformCloudConnectionMethod.ApiToken), + credentials: TerraformCloudConnectionAccessTokenCredentialsSchema.pick({}) + }) +]); + +export const ValidateTerraformCloudConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: z + .literal(TerraformCloudConnectionMethod.ApiToken) + .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() +}); diff --git a/backend/src/services/app-connection/terraform-cloud/terraform-cloud-connection-service.ts b/backend/src/services/app-connection/terraform-cloud/terraform-cloud-connection-service.ts new file mode 100644 index 000000000..56d56492b --- /dev/null +++ b/backend/src/services/app-connection/terraform-cloud/terraform-cloud-connection-service.ts @@ -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; + +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 + }; +}; diff --git a/backend/src/services/app-connection/terraform-cloud/terraform-cloud-connection-types.ts b/backend/src/services/app-connection/terraform-cloud/terraform-cloud-connection-types.ts new file mode 100644 index 000000000..cabcbb146 --- /dev/null +++ b/backend/src/services/app-connection/terraform-cloud/terraform-cloud-connection-types.ts @@ -0,0 +1,45 @@ +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; + +export type TTerraformCloudConnectionInput = z.infer & { + app: AppConnection.TerraformCloud; +}; + +export type TValidateTerraformCloudConnectionCredentialsSchema = + typeof ValidateTerraformCloudConnectionCredentialsSchema; + +export type TTerraformCloudConnectionConfig = DiscriminativePick< + TTerraformCloudConnectionInput, + "method" | "app" | "credentials" +> & { + orgId: string; +}; + +export type TTerraformCloudVariableSet = { + id: string; + name: string; + description?: string; + global?: boolean; +}; + +export type TTerraformCloudWorkspace = { + id: string; + name: string; +}; + +export type TTerraformCloudOrganization = { + id: string; + name: string; + variableSets: TTerraformCloudVariableSet[]; + workspaces: TTerraformCloudWorkspace[]; +}; diff --git a/backend/src/services/secret-sync/secret-sync-enums.ts b/backend/src/services/secret-sync/secret-sync-enums.ts index 70febfc0d..9349e2197 100644 --- a/backend/src/services/secret-sync/secret-sync-enums.ts +++ b/backend/src/services/secret-sync/secret-sync-enums.ts @@ -7,6 +7,7 @@ export enum SecretSync { AzureAppConfiguration = "azure-app-configuration", Databricks = "databricks", Humanitec = "humanitec", + TerraformCloud = "terraform-cloud", Camunda = "camunda", Vercel = "vercel" } diff --git a/backend/src/services/secret-sync/secret-sync-fns.ts b/backend/src/services/secret-sync/secret-sync-fns.ts index ea22ebffc..a3efa4bc1 100644 --- a/backend/src/services/secret-sync/secret-sync-fns.ts +++ b/backend/src/services/secret-sync/secret-sync-fns.ts @@ -27,6 +27,7 @@ import { GCP_SYNC_LIST_OPTION } from "./gcp"; import { GcpSyncFns } from "./gcp/gcp-sync-fns"; import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec"; import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns"; +import { TERRAFORM_CLOUD_SYNC_LIST_OPTION, TerraformCloudSyncFns } from "./terraform-cloud"; import { VERCEL_SYNC_LIST_OPTION, VercelSyncFns } from "./vercel"; const SECRET_SYNC_LIST_OPTIONS: Record = { @@ -38,6 +39,7 @@ const SECRET_SYNC_LIST_OPTIONS: Record = { [SecretSync.AzureAppConfiguration]: AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION, [SecretSync.Databricks]: DATABRICKS_SYNC_LIST_OPTION, [SecretSync.Humanitec]: HUMANITEC_SYNC_LIST_OPTION, + [SecretSync.TerraformCloud]: TERRAFORM_CLOUD_SYNC_LIST_OPTION, [SecretSync.Camunda]: CAMUNDA_SYNC_LIST_OPTION, [SecretSync.Vercel]: VERCEL_SYNC_LIST_OPTION }; @@ -125,6 +127,8 @@ export const SecretSyncFns = { }).syncSecrets(secretSync, secretMap); case SecretSync.Humanitec: return HumanitecSyncFns.syncSecrets(secretSync, secretMap); + case SecretSync.TerraformCloud: + return TerraformCloudSyncFns.syncSecrets(secretSync, secretMap); case SecretSync.Camunda: return camundaSyncFactory({ appConnectionDAL, @@ -176,6 +180,9 @@ export const SecretSyncFns = { case SecretSync.Humanitec: secretMap = await HumanitecSyncFns.getSecrets(secretSync); break; + case SecretSync.TerraformCloud: + secretMap = await TerraformCloudSyncFns.getSecrets(secretSync); + break; case SecretSync.Camunda: secretMap = await camundaSyncFactory({ appConnectionDAL, @@ -227,6 +234,8 @@ export const SecretSyncFns = { }).removeSecrets(secretSync, secretMap); case SecretSync.Humanitec: return HumanitecSyncFns.removeSecrets(secretSync, secretMap); + case SecretSync.TerraformCloud: + return TerraformCloudSyncFns.removeSecrets(secretSync, secretMap); case SecretSync.Camunda: return camundaSyncFactory({ appConnectionDAL, diff --git a/backend/src/services/secret-sync/secret-sync-maps.ts b/backend/src/services/secret-sync/secret-sync-maps.ts index 10e89ff03..661815814 100644 --- a/backend/src/services/secret-sync/secret-sync-maps.ts +++ b/backend/src/services/secret-sync/secret-sync-maps.ts @@ -10,6 +10,7 @@ export const SECRET_SYNC_NAME_MAP: Record = { [SecretSync.AzureAppConfiguration]: "Azure App Configuration", [SecretSync.Databricks]: "Databricks", [SecretSync.Humanitec]: "Humanitec", + [SecretSync.TerraformCloud]: "Terraform Cloud", [SecretSync.Camunda]: "Camunda", [SecretSync.Vercel]: "Vercel" }; @@ -23,6 +24,7 @@ export const SECRET_SYNC_CONNECTION_MAP: Record = { [SecretSync.AzureAppConfiguration]: AppConnection.AzureAppConfiguration, [SecretSync.Databricks]: AppConnection.Databricks, [SecretSync.Humanitec]: AppConnection.Humanitec, + [SecretSync.TerraformCloud]: AppConnection.TerraformCloud, [SecretSync.Camunda]: AppConnection.Camunda, [SecretSync.Vercel]: AppConnection.Vercel }; diff --git a/backend/src/services/secret-sync/secret-sync-types.ts b/backend/src/services/secret-sync/secret-sync-types.ts index 091f91240..d3207918c 100644 --- a/backend/src/services/secret-sync/secret-sync-types.ts +++ b/backend/src/services/secret-sync/secret-sync-types.ts @@ -55,6 +55,12 @@ import { THumanitecSyncListItem, THumanitecSyncWithCredentials } from "./humanitec"; +import { + TTerraformCloudSync, + TTerraformCloudSyncInput, + TTerraformCloudSyncListItem, + TTerraformCloudSyncWithCredentials +} from "./terraform-cloud"; import { TVercelSync, TVercelSyncInput, TVercelSyncListItem, TVercelSyncWithCredentials } from "./vercel"; export type TSecretSync = @@ -66,6 +72,7 @@ export type TSecretSync = | TAzureAppConfigurationSync | TDatabricksSync | THumanitecSync + | TTerraformCloudSync | TCamundaSync | TVercelSync; @@ -78,6 +85,7 @@ export type TSecretSyncWithCredentials = | TAzureAppConfigurationSyncWithCredentials | TDatabricksSyncWithCredentials | THumanitecSyncWithCredentials + | TTerraformCloudSyncWithCredentials | TCamundaSyncWithCredentials | TVercelSyncWithCredentials; @@ -90,6 +98,7 @@ export type TSecretSyncInput = | TAzureAppConfigurationSyncInput | TDatabricksSyncInput | THumanitecSyncInput + | TTerraformCloudSyncInput | TCamundaSyncInput | TVercelSyncInput; @@ -102,6 +111,7 @@ export type TSecretSyncListItem = | TAzureAppConfigurationSyncListItem | TDatabricksSyncListItem | THumanitecSyncListItem + | TTerraformCloudSyncListItem | TCamundaSyncListItem | TVercelSyncListItem; diff --git a/backend/src/services/secret-sync/terraform-cloud/index.ts b/backend/src/services/secret-sync/terraform-cloud/index.ts new file mode 100644 index 000000000..2df19747d --- /dev/null +++ b/backend/src/services/secret-sync/terraform-cloud/index.ts @@ -0,0 +1,5 @@ +export * from "./terraform-cloud-sync-constants"; +export * from "./terraform-cloud-sync-enums"; +export * from "./terraform-cloud-sync-fns"; +export * from "./terraform-cloud-sync-schemas"; +export * from "./terraform-cloud-sync-types"; diff --git a/backend/src/services/secret-sync/terraform-cloud/terraform-cloud-sync-constants.ts b/backend/src/services/secret-sync/terraform-cloud/terraform-cloud-sync-constants.ts new file mode 100644 index 000000000..edca7d304 --- /dev/null +++ b/backend/src/services/secret-sync/terraform-cloud/terraform-cloud-sync-constants.ts @@ -0,0 +1,10 @@ +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types"; + +export const TERRAFORM_CLOUD_SYNC_LIST_OPTION: TSecretSyncListItem = { + name: "Terraform Cloud", + destination: SecretSync.TerraformCloud, + connection: AppConnection.TerraformCloud, + canImportSecrets: false +}; diff --git a/backend/src/services/secret-sync/terraform-cloud/terraform-cloud-sync-enums.ts b/backend/src/services/secret-sync/terraform-cloud/terraform-cloud-sync-enums.ts new file mode 100644 index 000000000..cfd1daf2c --- /dev/null +++ b/backend/src/services/secret-sync/terraform-cloud/terraform-cloud-sync-enums.ts @@ -0,0 +1,9 @@ +export enum TerraformCloudSyncScope { + VariableSet = "variable-set", + Workspace = "workspace" +} + +export enum TerraformCloudSyncCategory { + Environment = "env", + Terraform = "terraform" +} diff --git a/backend/src/services/secret-sync/terraform-cloud/terraform-cloud-sync-fns.ts b/backend/src/services/secret-sync/terraform-cloud/terraform-cloud-sync-fns.ts new file mode 100644 index 000000000..4cfd7ec05 --- /dev/null +++ b/backend/src/services/secret-sync/terraform-cloud/terraform-cloud-sync-fns.ts @@ -0,0 +1,253 @@ +/* eslint-disable no-await-in-loop */ +import { AxiosResponse } from "axios"; + +import { request } from "@app/lib/config/request"; +import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; +import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; +import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; + +import { SECRET_SYNC_NAME_MAP } from "../secret-sync-maps"; +import { TerraformCloudSyncScope } from "./terraform-cloud-sync-enums"; +import { + TerraformCloudApiResponse, + TerraformCloudApiVariable, + TerraformCloudVariable, + TTerraformCloudSyncWithCredentials +} from "./terraform-cloud-sync-types"; + +const getTerraformCloudVariables = async ( + secretSync: TTerraformCloudSyncWithCredentials +): Promise => { + const { + destinationConfig, + connection: { + credentials: { apiToken } + } + } = secretSync; + + let url: string; + let source: TerraformCloudVariable["source"]; + + if (destinationConfig.scope === TerraformCloudSyncScope.VariableSet) { + url = `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/varsets/${destinationConfig.variableSetId}/relationships/vars`; + source = "varset"; + } else { + url = `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${destinationConfig.workspaceId}/vars`; + source = "workspace"; + } + + const headers = { + Authorization: `Bearer ${apiToken}`, + "Content-Type": "application/vnd.api+json" + }; + + const fetchAllPages = async (): Promise => { + let results: TerraformCloudApiVariable[] = []; + let nextUrl: string | null = url; + + while (nextUrl) { + const res: AxiosResponse> = await request.get< + TerraformCloudApiResponse + >(nextUrl, { + headers + }); + + if (res.data?.data) { + results = results.concat(res.data.data); + } + + nextUrl = res.data?.links?.next ?? null; + } + + return results; + }; + + const allVariableData = await fetchAllPages(); + + const variables: TerraformCloudVariable[] = allVariableData.map((variable) => ({ + id: variable.id, + key: variable.attributes.key, + value: variable.attributes.value || "", + sensitive: variable.attributes.sensitive, + description: variable.attributes.description || "", + category: variable.attributes.category, + source + })); + + return variables; +}; + +const deleteVariable = async ( + secretSync: TTerraformCloudSyncWithCredentials, + variable: TerraformCloudVariable +): Promise => { + const { + destinationConfig, + connection: { + credentials: { apiToken } + } + } = secretSync; + + try { + let url; + + if (destinationConfig.scope === TerraformCloudSyncScope.VariableSet) { + url = `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/varsets/${destinationConfig.variableSetId}/relationships/vars/${variable.id}`; + } else { + url = `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${destinationConfig.workspaceId}/vars/${variable.id}`; + } + + await request.delete(url, { + headers: { + Authorization: `Bearer ${apiToken}`, + "Content-Type": "application/vnd.api+json" + } + }); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: variable.key + }); + } +}; + +const createVariable = async ( + secretSync: TTerraformCloudSyncWithCredentials, + secretMap: TSecretMap, + key: string +): Promise => { + try { + const { + destinationConfig, + connection: { + credentials: { apiToken } + } + } = secretSync; + + let url; + + if (destinationConfig.scope === TerraformCloudSyncScope.VariableSet) { + url = `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/varsets/${destinationConfig.variableSetId}/relationships/vars`; + } else { + url = `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${destinationConfig.workspaceId}/vars`; + } + + await request.post( + url, + { + data: { + type: "vars", + attributes: { + key, + value: secretMap[key].value, + description: secretMap[key].comment || "", + category: secretSync.destinationConfig.category, + sensitive: true + } + } + }, + { + headers: { + Authorization: `Bearer ${apiToken}`, + "Content-Type": "application/vnd.api+json" + } + } + ); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); + } +}; + +const updateVariable = async ( + secretSync: TTerraformCloudSyncWithCredentials, + secretMap: TSecretMap, + variable: TerraformCloudVariable +): Promise => { + try { + const { + destinationConfig, + connection: { + credentials: { apiToken } + } + } = secretSync; + + let url; + + if (destinationConfig.scope === TerraformCloudSyncScope.VariableSet) { + url = `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/varsets/${destinationConfig.variableSetId}/relationships/vars/${variable.id}`; + } else { + url = `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${destinationConfig.workspaceId}/vars/${variable.id}`; + } + + await request.patch( + url, + { + data: { + type: "vars", + id: variable.id, + attributes: { + value: secretMap[variable.key].value, + description: secretMap[variable.key].comment || "", + category: secretSync.destinationConfig.category + } + } + }, + { + headers: { + Authorization: `Bearer ${apiToken}`, + "Content-Type": "application/vnd.api+json" + } + } + ); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: variable.key + }); + } +}; + +export const TerraformCloudSyncFns = { + syncSecrets: async (secretSync: TTerraformCloudSyncWithCredentials, secretMap: TSecretMap): Promise => { + const terraformCloudVariables = await getTerraformCloudVariables(secretSync); + const terraformCloudVariablesMap = new Map( + terraformCloudVariables.map((v) => [v.key, v]) + ); + + const secretKeys = Object.keys(secretMap); + for (const key of secretKeys) { + const existingVariable = terraformCloudVariablesMap.get(key); + + if (!existingVariable) { + await createVariable(secretSync, secretMap, key); + } else { + await updateVariable(secretSync, secretMap, existingVariable); + } + } + + if (secretSync.syncOptions.disableSecretDeletion) return; + + for (const terraformCloudVariable of terraformCloudVariables) { + if (!Object.prototype.hasOwnProperty.call(secretMap, terraformCloudVariable.key)) { + await deleteVariable(secretSync, terraformCloudVariable); + } + } + }, + + getSecrets: async (secretSync: TTerraformCloudSyncWithCredentials): Promise => { + throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`); + }, + + removeSecrets: async (secretSync: TTerraformCloudSyncWithCredentials, secretMap: TSecretMap): Promise => { + const terraformCloudVariables = await getTerraformCloudVariables(secretSync); + + for (const variable of terraformCloudVariables) { + if (Object.prototype.hasOwnProperty.call(secretMap, variable.key)) { + await deleteVariable(secretSync, variable); + } + } + } +}; diff --git a/backend/src/services/secret-sync/terraform-cloud/terraform-cloud-sync-schemas.ts b/backend/src/services/secret-sync/terraform-cloud/terraform-cloud-sync-schemas.ts new file mode 100644 index 000000000..359d7f4c5 --- /dev/null +++ b/backend/src/services/secret-sync/terraform-cloud/terraform-cloud-sync-schemas.ts @@ -0,0 +1,77 @@ +import { z } from "zod"; + +import { SecretSyncs } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { + BaseSecretSyncSchema, + GenericCreateSecretSyncFieldsSchema, + GenericUpdateSecretSyncFieldsSchema +} from "@app/services/secret-sync/secret-sync-schemas"; +import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; +import { + TerraformCloudSyncCategory, + TerraformCloudSyncScope +} from "@app/services/secret-sync/terraform-cloud/terraform-cloud-sync-enums"; + +const TerraformCloudSyncDestinationConfigSchema = z.discriminatedUnion("scope", [ + z.object({ + scope: z + .literal(TerraformCloudSyncScope.VariableSet) + .describe(SecretSyncs.DESTINATION_CONFIG.TERRAFORM_CLOUD.scope), + org: z.string().min(1, "Org ID is required").describe(SecretSyncs.DESTINATION_CONFIG.TERRAFORM_CLOUD.org), + variableSetName: z + .string() + .min(1, "Variable set name is required") + .describe(SecretSyncs.DESTINATION_CONFIG.TERRAFORM_CLOUD.variableSetName), + variableSetId: z + .string() + .min(1, "Variable set ID is required") + .describe(SecretSyncs.DESTINATION_CONFIG.TERRAFORM_CLOUD.variableSetId), + category: z.nativeEnum(TerraformCloudSyncCategory).describe(SecretSyncs.DESTINATION_CONFIG.TERRAFORM_CLOUD.category) + }), + z.object({ + scope: z.literal(TerraformCloudSyncScope.Workspace).describe(SecretSyncs.DESTINATION_CONFIG.TERRAFORM_CLOUD.scope), + org: z.string().min(1, "Org ID is required").describe(SecretSyncs.DESTINATION_CONFIG.TERRAFORM_CLOUD.org), + workspaceName: z + .string() + .min(1, "Workspace name is required") + .describe(SecretSyncs.DESTINATION_CONFIG.TERRAFORM_CLOUD.workspaceName), + workspaceId: z + .string() + .min(1, "Workspace ID is required") + .describe(SecretSyncs.DESTINATION_CONFIG.TERRAFORM_CLOUD.workspaceId), + category: z.nativeEnum(TerraformCloudSyncCategory).describe(SecretSyncs.DESTINATION_CONFIG.TERRAFORM_CLOUD.category) + }) +]); + +const TerraformCloudSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false }; + +export const TerraformCloudSyncSchema = BaseSecretSyncSchema( + SecretSync.TerraformCloud, + TerraformCloudSyncOptionsConfig +).extend({ + destination: z.literal(SecretSync.TerraformCloud), + destinationConfig: TerraformCloudSyncDestinationConfigSchema +}); + +export const CreateTerraformCloudSyncSchema = GenericCreateSecretSyncFieldsSchema( + SecretSync.TerraformCloud, + TerraformCloudSyncOptionsConfig +).extend({ + destinationConfig: TerraformCloudSyncDestinationConfigSchema +}); + +export const UpdateTerraformCloudSyncSchema = GenericUpdateSecretSyncFieldsSchema( + SecretSync.TerraformCloud, + TerraformCloudSyncOptionsConfig +).extend({ + destinationConfig: TerraformCloudSyncDestinationConfigSchema.optional() +}); + +export const TerraformCloudSyncListItemSchema = z.object({ + name: z.literal("Terraform Cloud"), + connection: z.literal(AppConnection.TerraformCloud), + destination: z.literal(SecretSync.TerraformCloud), + canImportSecrets: z.literal(false) +}); diff --git a/backend/src/services/secret-sync/terraform-cloud/terraform-cloud-sync-types.ts b/backend/src/services/secret-sync/terraform-cloud/terraform-cloud-sync-types.ts new file mode 100644 index 000000000..f68db0d51 --- /dev/null +++ b/backend/src/services/secret-sync/terraform-cloud/terraform-cloud-sync-types.ts @@ -0,0 +1,77 @@ +import z from "zod"; + +import { TTerraformCloudConnection } from "@app/services/app-connection/terraform-cloud"; + +import { + CreateTerraformCloudSyncSchema, + TerraformCloudSyncListItemSchema, + TerraformCloudSyncSchema +} from "./terraform-cloud-sync-schemas"; + +export type TTerraformCloudSyncListItem = z.infer; + +export type TTerraformCloudSync = z.infer; + +export type TTerraformCloudSyncInput = z.infer; + +export type TTerraformCloudSyncWithCredentials = TTerraformCloudSync & { + connection: TTerraformCloudConnection; +}; + +export type TerraformCloudApiVariable = { + id: string; + type: string; + attributes: { + key: string; + value: string | null; + sensitive: boolean; + category: "terraform" | "env"; + hcl: boolean; + description: string | null; + }; + relationships: { + workspace?: { + data: { + id: string; + type: string; + }; + }; + project?: { + data: { + id: string; + type: string; + }; + }; + }; +}; + +export type TerraformCloudVariable = { + id: string; + key: string; + value: string; + sensitive: boolean; + description: string; + category: "terraform" | "env"; + source: "varset" | "workspace"; +}; + +export type TerraformCloudApiResponse = { + data: T; + included?: unknown[]; + links?: { + self?: string; + first?: string; + prev?: string; + next?: string; + last?: string; + }; + meta?: { + pagination?: { + current_page: number; + prev_page: number | null; + next_page: number | null; + total_pages: number; + total_count: number; + }; + }; +}; diff --git a/docs/api-reference/endpoints/app-connections/terraform-cloud/available.mdx b/docs/api-reference/endpoints/app-connections/terraform-cloud/available.mdx new file mode 100644 index 000000000..fb2dbdeaf --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/terraform-cloud/available.mdx @@ -0,0 +1,4 @@ +--- +title: "Available" +openapi: "GET /api/v1/app-connections/terraform-cloud/available" +--- diff --git a/docs/api-reference/endpoints/app-connections/terraform-cloud/create.mdx b/docs/api-reference/endpoints/app-connections/terraform-cloud/create.mdx new file mode 100644 index 000000000..ad7d4a5d1 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/terraform-cloud/create.mdx @@ -0,0 +1,9 @@ +--- +title: "Create" +openapi: "POST /api/v1/app-connections/terraform-cloud" +--- + + + Check out the configuration docs for [Terraform Cloud Connections](/integrations/app-connections/terraform-cloud) to learn how to obtain + the required credentials. + \ No newline at end of file diff --git a/docs/api-reference/endpoints/app-connections/terraform-cloud/delete.mdx b/docs/api-reference/endpoints/app-connections/terraform-cloud/delete.mdx new file mode 100644 index 000000000..daa558f5a --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/terraform-cloud/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/terraform-cloud/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/terraform-cloud/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/terraform-cloud/get-by-id.mdx new file mode 100644 index 000000000..587cc8f11 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/terraform-cloud/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/terraform-cloud/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/terraform-cloud/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/terraform-cloud/get-by-name.mdx new file mode 100644 index 000000000..722381605 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/terraform-cloud/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/terraform-cloud/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/terraform-cloud/list.mdx b/docs/api-reference/endpoints/app-connections/terraform-cloud/list.mdx new file mode 100644 index 000000000..831846155 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/terraform-cloud/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/terraform-cloud" +--- diff --git a/docs/api-reference/endpoints/app-connections/terraform-cloud/update.mdx b/docs/api-reference/endpoints/app-connections/terraform-cloud/update.mdx new file mode 100644 index 000000000..b8f526a88 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/terraform-cloud/update.mdx @@ -0,0 +1,9 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/terraform-cloud/{connectionId}" +--- + + + Check out the configuration docs for [Terraform Cloud Connections](/integrations/app-connections/terraform-cloud) to learn how to obtain + the required credentials. + \ No newline at end of file diff --git a/docs/api-reference/endpoints/secret-syncs/terraform-cloud/create.mdx b/docs/api-reference/endpoints/secret-syncs/terraform-cloud/create.mdx new file mode 100644 index 000000000..491889e16 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/terraform-cloud/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-syncs/terraform-cloud" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/terraform-cloud/delete.mdx b/docs/api-reference/endpoints/secret-syncs/terraform-cloud/delete.mdx new file mode 100644 index 000000000..dfd3206f5 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/terraform-cloud/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-syncs/terraform-cloud/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/terraform-cloud/get-by-id.mdx b/docs/api-reference/endpoints/secret-syncs/terraform-cloud/get-by-id.mdx new file mode 100644 index 000000000..c25888a53 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/terraform-cloud/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/secret-syncs/terraform-cloud/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/terraform-cloud/get-by-name.mdx b/docs/api-reference/endpoints/secret-syncs/terraform-cloud/get-by-name.mdx new file mode 100644 index 000000000..5a1645866 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/terraform-cloud/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/secret-syncs/terraform-cloud/sync-name/{syncName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/terraform-cloud/list.mdx b/docs/api-reference/endpoints/secret-syncs/terraform-cloud/list.mdx new file mode 100644 index 000000000..0993c76ef --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/terraform-cloud/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-syncs/terraform-cloud" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/terraform-cloud/remove-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/terraform-cloud/remove-secrets.mdx new file mode 100644 index 000000000..6f00362e3 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/terraform-cloud/remove-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Secrets" +openapi: "POST /api/v1/secret-syncs/terraform-cloud/{syncId}/remove-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/terraform-cloud/sync-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/terraform-cloud/sync-secrets.mdx new file mode 100644 index 000000000..c71b68e48 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/terraform-cloud/sync-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Secrets" +openapi: "POST /api/v1/secret-syncs/terraform-cloud/{syncId}/sync-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/terraform-cloud/update.mdx b/docs/api-reference/endpoints/secret-syncs/terraform-cloud/update.mdx new file mode 100644 index 000000000..759fcfc71 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/terraform-cloud/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/secret-syncs/terraform-cloud/{syncId}" +--- diff --git a/docs/images/app-connections/terraform-cloud/terraform-cloud-account-settings.png b/docs/images/app-connections/terraform-cloud/terraform-cloud-account-settings.png new file mode 100644 index 000000000..f80df4229 Binary files /dev/null and b/docs/images/app-connections/terraform-cloud/terraform-cloud-account-settings.png differ diff --git a/docs/images/app-connections/terraform-cloud/terraform-cloud-app-connection-created.png b/docs/images/app-connections/terraform-cloud/terraform-cloud-app-connection-created.png new file mode 100644 index 000000000..f8904957a Binary files /dev/null and b/docs/images/app-connections/terraform-cloud/terraform-cloud-app-connection-created.png differ diff --git a/docs/images/app-connections/terraform-cloud/terraform-cloud-app-connection-modal.png b/docs/images/app-connections/terraform-cloud/terraform-cloud-app-connection-modal.png new file mode 100644 index 000000000..e8f0b9524 Binary files /dev/null and b/docs/images/app-connections/terraform-cloud/terraform-cloud-app-connection-modal.png differ diff --git a/docs/images/app-connections/terraform-cloud/terraform-cloud-app-connection-option.png b/docs/images/app-connections/terraform-cloud/terraform-cloud-app-connection-option.png new file mode 100644 index 000000000..369067a31 Binary files /dev/null and b/docs/images/app-connections/terraform-cloud/terraform-cloud-app-connection-option.png differ diff --git a/docs/images/app-connections/terraform-cloud/terraform-cloud-copy-api-token.png b/docs/images/app-connections/terraform-cloud/terraform-cloud-copy-api-token.png new file mode 100644 index 000000000..348a4fc53 Binary files /dev/null and b/docs/images/app-connections/terraform-cloud/terraform-cloud-copy-api-token.png differ diff --git a/docs/images/app-connections/terraform-cloud/terraform-cloud-create-api-token.png b/docs/images/app-connections/terraform-cloud/terraform-cloud-create-api-token.png new file mode 100644 index 000000000..3637145b6 Binary files /dev/null and b/docs/images/app-connections/terraform-cloud/terraform-cloud-create-api-token.png differ diff --git a/docs/images/app-connections/terraform-cloud/terraform-cloud-tokens-tab.png b/docs/images/app-connections/terraform-cloud/terraform-cloud-tokens-tab.png new file mode 100644 index 000000000..3293ccdac Binary files /dev/null and b/docs/images/app-connections/terraform-cloud/terraform-cloud-tokens-tab.png differ diff --git a/docs/images/secret-syncs/terraform-cloud/terraform-cloud-created.png b/docs/images/secret-syncs/terraform-cloud/terraform-cloud-created.png new file mode 100644 index 000000000..d6a89609e Binary files /dev/null and b/docs/images/secret-syncs/terraform-cloud/terraform-cloud-created.png differ diff --git a/docs/images/secret-syncs/terraform-cloud/terraform-cloud-destination.png b/docs/images/secret-syncs/terraform-cloud/terraform-cloud-destination.png new file mode 100644 index 000000000..bb2e2f095 Binary files /dev/null and b/docs/images/secret-syncs/terraform-cloud/terraform-cloud-destination.png differ diff --git a/docs/images/secret-syncs/terraform-cloud/terraform-cloud-details.png b/docs/images/secret-syncs/terraform-cloud/terraform-cloud-details.png new file mode 100644 index 000000000..b87c51494 Binary files /dev/null and b/docs/images/secret-syncs/terraform-cloud/terraform-cloud-details.png differ diff --git a/docs/images/secret-syncs/terraform-cloud/terraform-cloud-option.png b/docs/images/secret-syncs/terraform-cloud/terraform-cloud-option.png new file mode 100644 index 000000000..7670bf2c1 Binary files /dev/null and b/docs/images/secret-syncs/terraform-cloud/terraform-cloud-option.png differ diff --git a/docs/images/secret-syncs/terraform-cloud/terraform-cloud-options.png b/docs/images/secret-syncs/terraform-cloud/terraform-cloud-options.png new file mode 100644 index 000000000..def9cf1c0 Binary files /dev/null and b/docs/images/secret-syncs/terraform-cloud/terraform-cloud-options.png differ diff --git a/docs/images/secret-syncs/terraform-cloud/terraform-cloud-review.png b/docs/images/secret-syncs/terraform-cloud/terraform-cloud-review.png new file mode 100644 index 000000000..f7b245771 Binary files /dev/null and b/docs/images/secret-syncs/terraform-cloud/terraform-cloud-review.png differ diff --git a/docs/images/secret-syncs/terraform-cloud/terraform-cloud-source.png b/docs/images/secret-syncs/terraform-cloud/terraform-cloud-source.png new file mode 100644 index 000000000..7a7250f88 Binary files /dev/null and b/docs/images/secret-syncs/terraform-cloud/terraform-cloud-source.png differ diff --git a/docs/integrations/app-connections/terraform-cloud.mdx b/docs/integrations/app-connections/terraform-cloud.mdx new file mode 100644 index 000000000..02deb22cc --- /dev/null +++ b/docs/integrations/app-connections/terraform-cloud.mdx @@ -0,0 +1,83 @@ +--- +title: "Terraform Cloud Connection" +description: "Learn how to configure a Terraform Cloud Connection for Infisical." +--- + +Infisical supports connecting to Terraform Cloud using a service user. + +## Setup Terraform Cloud Connection in Infisical + + + + Navigate to the Terraform Cloud **Account Settings** tab. + ![Terraform Cloud Account Settings](/images/app-connections/terraform-cloud/terraform-cloud-account-settings.png) + + + Move to the **Tokens** tab. + ![Terraform Cloud Tokens Tab](/images/app-connections/terraform-cloud/terraform-cloud-tokens-tab.png) + + + Create the API token to be used by Infisical. + + If you configure an expiry date for your API token you will need to manually rotate to a new token prior to expiration to avoid integration downtime. + + ![Terraform Cloud Create API Token](/images/app-connections/terraform-cloud/terraform-cloud-create-api-token.png) + + + The API token will be displayed after creating it. Save the token in a secure location for later use in the following steps. + ![Terraform Cloud Copy API Token](/images/app-connections/terraform-cloud/terraform-cloud-copy-api-token.png) + + + + + 1. Navigate to the **App Connections** tab on the **Organization Settings** page. + ![App Connections Tab](/images/app-connections/general/add-connection.png) + 2. Select the **Terraform Cloud Connection** option from the connection options modal. + ![Select Terraform Cloud Connection](/images/app-connections/terraform-cloud/terraform-cloud-app-connection-option.png) + 3. Fill out the Terraform Cloud Connection modal, here you will need to provide the API Token generated in the previous step. + ![Terraform Cloud Connection Modal](/images/app-connections/terraform-cloud/terraform-cloud-app-connection-modal.png) + 4. Your **Terraform Cloud Connection** is now available for use. + ![Terraform Cloud Connection Created](/images/app-connections/terraform-cloud/terraform-cloud-app-connection-created.png) + + + To create an Terraform Cloud Connection, make an API request to the [Create Terraform Cloud + Connection](/api-reference/endpoints/app-connections/terraform-cloud/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/app-connections/terraform-cloud \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-terraform-cloud-connection", + "method": "api-token", + "credentials": { + "apiToken": "...", + } + }' + ``` + + ### Sample response + + ```bash Response + { + "appConnection": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-terraform-cloud-connection", + "version": 123, + "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "app": "terraform-cloud", + "method": "api-token", + "credentials": { + "apiToken": "..." + } + } + } + ``` + + + + diff --git a/docs/integrations/secret-syncs/terraform-cloud.mdx b/docs/integrations/secret-syncs/terraform-cloud.mdx new file mode 100644 index 000000000..80a087d2b --- /dev/null +++ b/docs/integrations/secret-syncs/terraform-cloud.mdx @@ -0,0 +1,161 @@ +--- +title: "Terraform Cloud Sync" +description: "Learn how to configure a Terraform Cloud Sync for Infisical." +--- + +**Prerequisites:** + + - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) + - Create a [Terraform Cloud Connection](/integrations/app-connections/terraform-cloud) + + + + 1. Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button. + ![Secret Syncs Tab](/images/secret-syncs/general/secret-sync-tab.png) + + 2. Select the **Terraform Cloud** option. + ![Select Terraform Cloud](/images/secret-syncs/terraform-cloud/terraform-cloud-option.png) + + 3. Configure the **Source** from where secrets should be retrieved, then click **Next**. + ![Configure Source](/images/secret-syncs/terraform-cloud/terraform-cloud-source.png) + + - **Environment**: The project environment to retrieve secrets from. + - **Secret Path**: The folder path to retrieve secrets from. + + + If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports). + + + 4. Configure the **Destination** to where secrets should be deployed, then click **Next**. + ![Configure Destination](/images/secret-syncs/terraform-cloud/terraform-cloud-destination.png) + + - **Terraform Cloud Connection**: The Terraform Cloud Connection to authenticate with. + - **Organization**: The Terraform Cloud organization to deploy secrets to. + - **Category**: The Terraform Cloud variable category to use on secrets syncs. Choose from: + - **Environment**: Sync secrets as environment variables. + - **Terraform**: Sync secrets as Terraform variables. + - **Scope**: The Terraform Cloud secret scope to sync secrets to. + - **Variable Set**: Sync secrets to a specific variable set. + - **Workspace**: Sync secrets to a specific workspace. +

+ The remaining fields are determined by the selected **Scope**: + + + - **Variable Set**: The variable set to deploy secrets to. + + + - **Workspace**: The workspace to deploy secrets to. + + + + 5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. + ![Configure Options](/images/secret-syncs/terraform-cloud/terraform-cloud-options.png) + + - **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync. + - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. + + Terraform Cloud does not support importing secrets. + + - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. + - **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical. + + 6. Configure the **Details** of your Terraform Cloud Sync, then click **Next**. + ![Configure Details](/images/secret-syncs/terraform-cloud/terraform-cloud-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + 7. Review your Terraform Cloud Sync configuration, then click **Create Sync**. + ![Confirm Configuration](/images/secret-syncs/terraform-cloud/terraform-cloud-review.png) + + 8. If enabled, your Terraform Cloud Sync will begin syncing your secrets to the destination endpoint. + ![Sync Secrets](/images/secret-syncs/terraform-cloud/terraform-cloud-created.png) + + + + To create an **Terraform Cloud Sync**, make an API request to the [Create Terraform Cloud Sync](/api-reference/endpoints/secret-syncs/terraform-cloud/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/secret-syncs/terraform-cloud \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-terraform-cloud-sync", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "an example sync", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "environment": "dev", + "secretPath": "/my-secrets", + "isEnabled": true, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "destinationConfig": { + "scope": "variable-set", + "variableSetId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "variableSetName": "my-variable-set", + "org": "my-organization-id", + "category": "env" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "secretSync": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-terraform-cloud-sync", + "description": "an example sync", + "isEnabled": true, + "version": 1, + "folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "syncStatus": "succeeded", + "lastSyncJobId": "123", + "lastSyncMessage": null, + "lastSyncedAt": "2023-11-07T05:31:56Z", + "importStatus": null, + "lastImportJobId": null, + "lastImportMessage": null, + "lastImportedAt": null, + "removeStatus": null, + "lastRemoveJobId": null, + "lastRemoveMessage": null, + "lastRemovedAt": null, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connection": { + "app": "terraform-cloud", + "name": "my-terraform-cloud-connection", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "environment": { + "slug": "dev", + "name": "Development", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "folder": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "path": "/my-secrets" + }, + "destination": "terraform-cloud", + "destinationConfig": { + "scope": "workspace", + "workspaceId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "workspaceName": "my-workspace", + "org": "my-organization-id", + "category": "terraform" + } + } + } + ``` + + diff --git a/docs/mint.json b/docs/mint.json index d0ad859b7..0ebc22e92 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -422,9 +422,10 @@ "integrations/app-connections/gcp", "integrations/app-connections/github", "integrations/app-connections/humanitec", - "integrations/app-connections/vercel", "integrations/app-connections/mssql", - "integrations/app-connections/postgres" + "integrations/app-connections/postgres", + "integrations/app-connections/terraform-cloud", + "integrations/app-connections/vercel" ] } ] @@ -445,6 +446,7 @@ "integrations/secret-syncs/gcp-secret-manager", "integrations/secret-syncs/github", "integrations/secret-syncs/humanitec", + "integrations/secret-syncs/terraform-cloud", "integrations/secret-syncs/vercel" ] } @@ -976,18 +978,6 @@ "api-reference/endpoints/app-connections/humanitec/delete" ] }, - { - "group": "Vercel", - "pages": [ - "api-reference/endpoints/app-connections/vercel/list", - "api-reference/endpoints/app-connections/vercel/available", - "api-reference/endpoints/app-connections/vercel/get-by-id", - "api-reference/endpoints/app-connections/vercel/get-by-name", - "api-reference/endpoints/app-connections/vercel/create", - "api-reference/endpoints/app-connections/vercel/update", - "api-reference/endpoints/app-connections/vercel/delete" - ] - }, { "group": "Microsoft SQL Server", "pages": [ @@ -1011,6 +1001,30 @@ "api-reference/endpoints/app-connections/postgres/update", "api-reference/endpoints/app-connections/postgres/delete" ] + }, + { + "group": "Terraform Cloud", + "pages": [ + "api-reference/endpoints/app-connections/terraform-cloud/list", + "api-reference/endpoints/app-connections/terraform-cloud/available", + "api-reference/endpoints/app-connections/terraform-cloud/get-by-id", + "api-reference/endpoints/app-connections/terraform-cloud/get-by-name", + "api-reference/endpoints/app-connections/terraform-cloud/create", + "api-reference/endpoints/app-connections/terraform-cloud/update", + "api-reference/endpoints/app-connections/terraform-cloud/delete" + ] + }, + { + "group": "Vercel", + "pages": [ + "api-reference/endpoints/app-connections/vercel/list", + "api-reference/endpoints/app-connections/vercel/available", + "api-reference/endpoints/app-connections/vercel/get-by-id", + "api-reference/endpoints/app-connections/vercel/get-by-name", + "api-reference/endpoints/app-connections/vercel/create", + "api-reference/endpoints/app-connections/vercel/update", + "api-reference/endpoints/app-connections/vercel/delete" + ] } ] }, @@ -1141,6 +1155,19 @@ "api-reference/endpoints/secret-syncs/humanitec/remove-secrets" ] }, + { + "group": "Terraform Cloud", + "pages": [ + "api-reference/endpoints/secret-syncs/terraform-cloud/list", + "api-reference/endpoints/secret-syncs/terraform-cloud/get-by-id", + "api-reference/endpoints/secret-syncs/terraform-cloud/get-by-name", + "api-reference/endpoints/secret-syncs/terraform-cloud/create", + "api-reference/endpoints/secret-syncs/terraform-cloud/update", + "api-reference/endpoints/secret-syncs/terraform-cloud/delete", + "api-reference/endpoints/secret-syncs/terraform-cloud/sync-secrets", + "api-reference/endpoints/secret-syncs/terraform-cloud/remove-secrets" + ] + }, { "group": "Vercel", "pages": [ diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx index d0a53019d..854c841dd 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx @@ -12,6 +12,7 @@ import { DatabricksSyncFields } from "./DatabricksSyncFields"; import { GcpSyncFields } from "./GcpSyncFields"; import { GitHubSyncFields } from "./GitHubSyncFields"; import { HumanitecSyncFields } from "./HumanitecSyncFields"; +import { TerraformCloudSyncFields } from "./TerraformCloudSyncFields"; import { VercelSyncFields } from "./VercelSyncFields"; export const SecretSyncDestinationFields = () => { @@ -36,6 +37,8 @@ export const SecretSyncDestinationFields = () => { return ; case SecretSync.Humanitec: return ; + case SecretSync.TerraformCloud: + return ; case SecretSync.Camunda: return ; case SecretSync.Vercel: diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/TerraformCloudSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/TerraformCloudSyncFields.tsx new file mode 100644 index 000000000..70394cc76 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/TerraformCloudSyncFields.tsx @@ -0,0 +1,244 @@ +import { Controller, useFormContext, useWatch } from "react-hook-form"; +import { SingleValue } from "react-select"; + +import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField"; +import { FilterableSelect, FormControl, Select, SelectItem } from "@app/components/v2"; +import { + TERRAFORM_CLOUD_SYNC_SCOPES, + TerraformCloudSyncCategory, + TerraformCloudSyncScope, + TTerraformCloudConnectionOrganization, + TTerraformCloudConnectionVariableSet, + TTerraformCloudConnectionWorkspace, + useTerraformCloudConnectionListOrganizations +} from "@app/hooks/api/appConnections/terraform-cloud"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +import { TSecretSyncForm } from "../schemas"; + +export const TerraformCloudSyncFields = () => { + const { control, watch, setValue } = useFormContext< + TSecretSyncForm & { destination: SecretSync.TerraformCloud } + >(); + + const connectionId = useWatch({ name: "connection.id", control }); + const currentOrg = watch("destinationConfig.org"); + const currentScope = watch("destinationConfig.scope"); + + const { data: organizations = [], isPending: isOrganizationsPending } = + useTerraformCloudConnectionListOrganizations(connectionId, { + enabled: Boolean(connectionId) + }); + + const selectedOrg = organizations?.find((org) => org.id === currentOrg); + const variableSets = selectedOrg?.variableSets || []; + const workspaces = selectedOrg?.workspaces || []; + + return ( + <> + { + setValue("destinationConfig.org", ""); + setValue("destinationConfig.variableSetId", ""); + setValue("destinationConfig.workspaceId", ""); + setValue("destinationConfig.variableSetName", ""); + setValue("destinationConfig.workspaceName", ""); + }} + /> + ( + + org.id === value) ?? null) : null} + onChange={(option) => { + onChange( + (option as SingleValue)?.id ?? null + ); + setValue("destinationConfig.variableSetId", ""); + setValue("destinationConfig.workspaceId", ""); + setValue("destinationConfig.variableSetName", ""); + setValue("destinationConfig.workspaceName", ""); + }} + options={organizations} + placeholder="Select an organization..." + getOptionLabel={(option) => option.name} + getOptionValue={(option) => option.id.toString()} + /> + + )} + /> + ( + +

    +
  • +

    + + Environment variables configure Terraform's behavior (e.g., + credentials). + +

    +
  • +
  • +

    + + Terraform variables are used as input values in your configuration. + +

    +
  • +
+ + } + > + + + )} + /> + ( + +

+ Specify how Infisical should manage secrets from Terraform Cloud. The following + options are available: +

+
    + {Object.values(TERRAFORM_CLOUD_SYNC_SCOPES).map(({ name, description }) => { + return ( +
  • +

    + {name}: {description} +

    +
  • + ); + })} +
+ + } + > + +
+ )} + /> + {currentScope === TerraformCloudSyncScope.VariableSet && ( + ( + + variableSet.id === value) ?? null} + onChange={(option) => { + const selectedOption = + option as SingleValue; + onChange(selectedOption?.id ?? null); + + if (selectedOption) { + setValue("destinationConfig.variableSetName", selectedOption.name); + } else { + setValue("destinationConfig.variableSetName", ""); + } + }} + options={variableSets} + placeholder="Select a variable set..." + getOptionLabel={(option) => option.name} + getOptionValue={(option) => option.id.toString()} + /> + + )} + /> + )} + {currentScope === TerraformCloudSyncScope.Workspace && ( + ( + + workspace.id === value) ?? null} + onChange={(option) => { + const selectedOption = option as SingleValue; + onChange(selectedOption?.id ?? null); + + if (selectedOption) { + setValue("destinationConfig.workspaceName", selectedOption.name); + } else { + setValue("destinationConfig.workspaceName", ""); + } + }} + options={workspaces} + placeholder="Select a workspace..." + getOptionLabel={(option) => option.name} + getOptionValue={(option) => option.id.toString()} + /> + + )} + /> + )} + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx index 6c09c65cc..5e8ff01d1 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx @@ -39,6 +39,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => { case SecretSync.AzureAppConfiguration: case SecretSync.Databricks: case SecretSync.Humanitec: + case SecretSync.TerraformCloud: case SecretSync.Camunda: case SecretSync.Vercel: AdditionalSyncOptionsFieldsComponent = null; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx index 573854906..4e9e89427 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx @@ -22,6 +22,7 @@ import { DatabricksSyncReviewFields } from "./DatabricksSyncReviewFields"; import { GcpSyncReviewFields } from "./GcpSyncReviewFields"; import { GitHubSyncReviewFields } from "./GitHubSyncReviewFields"; import { HumanitecSyncReviewFields } from "./HumanitecSyncReviewFields"; +import { TerraformCloudSyncReviewFields } from "./TerraformCloudSyncReviewFields"; import { VercelSyncReviewFields } from "./VercelSyncReviewFields"; export const SecretSyncReviewFields = () => { @@ -74,6 +75,9 @@ export const SecretSyncReviewFields = () => { case SecretSync.Humanitec: DestinationFieldsComponent = ; break; + case SecretSync.TerraformCloud: + DestinationFieldsComponent = ; + break; case SecretSync.Camunda: DestinationFieldsComponent = ; break; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/TerraformCloudSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/TerraformCloudSyncReviewFields.tsx new file mode 100644 index 000000000..614e3e180 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/TerraformCloudSyncReviewFields.tsx @@ -0,0 +1,28 @@ +import { useFormContext } from "react-hook-form"; + +import { GenericFieldLabel } from "@app/components/secret-syncs"; +import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas"; +import { TerraformCloudSyncScope } from "@app/hooks/api/appConnections/terraform-cloud"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +export const TerraformCloudSyncReviewFields = () => { + const { watch } = useFormContext(); + const orgId = watch("destinationConfig.org"); + const variableSetName = watch("destinationConfig.variableSetName"); + const workspaceName = watch("destinationConfig.workspaceName"); + const scope = watch("destinationConfig.scope"); + const category = watch("destinationConfig.category"); + + return ( + <> + {orgId} + {scope === TerraformCloudSyncScope.VariableSet && ( + {variableSetName} + )} + {scope === TerraformCloudSyncScope.Workspace && ( + {workspaceName} + )} + {category} + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts index e8f68de91..4eb1094ae 100644 --- a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts +++ b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts @@ -10,6 +10,7 @@ import { AzureKeyVaultSyncDestinationSchema } from "./azure-key-vault-sync-desti import { CamundaSyncDestinationSchema } from "./camunda-sync-destination-schema"; import { GcpSyncDestinationSchema } from "./gcp-sync-destination-schema"; import { HumanitecSyncDestinationSchema } from "./humanitec-sync-destination-schema"; +import { TerraformCloudSyncDestinationSchema } from "./terraform-cloud-destination-schema"; import { VercelSyncDestinationSchema } from "./vercel-sync-destination-schema"; const SecretSyncUnionSchema = z.discriminatedUnion("destination", [ @@ -21,6 +22,7 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [ AzureAppConfigurationSyncDestinationSchema, DatabricksSyncDestinationSchema, HumanitecSyncDestinationSchema, + TerraformCloudSyncDestinationSchema, CamundaSyncDestinationSchema, VercelSyncDestinationSchema ]); diff --git a/frontend/src/components/secret-syncs/forms/schemas/terraform-cloud-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/terraform-cloud-destination-schema.ts new file mode 100644 index 000000000..d818fd29a --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/schemas/terraform-cloud-destination-schema.ts @@ -0,0 +1,30 @@ +import { z } from "zod"; + +import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema"; +import { + TerraformCloudSyncCategory, + TerraformCloudSyncScope +} from "@app/hooks/api/appConnections/terraform-cloud"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +export const TerraformCloudSyncDestinationSchema = BaseSecretSyncSchema().merge( + z.object({ + destination: z.literal(SecretSync.TerraformCloud), + destinationConfig: z.discriminatedUnion("scope", [ + z.object({ + scope: z.literal(TerraformCloudSyncScope.VariableSet), + org: z.string().trim().min(1, "Organization required"), + variableSetId: z.string().trim().min(1, "Variable Set required"), + variableSetName: z.string().trim().min(1, "Variable set name required"), + category: z.nativeEnum(TerraformCloudSyncCategory) + }), + z.object({ + scope: z.literal(TerraformCloudSyncScope.Workspace), + org: z.string().trim().min(1, "Organization required"), + workspaceId: z.string().trim().min(1, "Workspace required"), + workspaceName: z.string().trim().min(1, "Workspace name required"), + category: z.nativeEnum(TerraformCloudSyncCategory) + }) + ]) + }) +); diff --git a/frontend/src/helpers/appConnections.ts b/frontend/src/helpers/appConnections.ts index cf87ae50e..53e2f5468 100644 --- a/frontend/src/helpers/appConnections.ts +++ b/frontend/src/helpers/appConnections.ts @@ -14,6 +14,7 @@ import { MsSqlConnectionMethod, PostgresConnectionMethod, TAppConnection, + TerraformCloudConnectionMethod, VercelConnectionMethod } from "@app/hooks/api/appConnections/types"; @@ -31,6 +32,7 @@ export const APP_CONNECTION_MAP: Record = { [SecretSync.AzureAppConfiguration]: AppConnection.AzureAppConfiguration, [SecretSync.Databricks]: AppConnection.Databricks, [SecretSync.Humanitec]: AppConnection.Humanitec, + [SecretSync.TerraformCloud]: AppConnection.TerraformCloud, [SecretSync.Camunda]: AppConnection.Camunda, [SecretSync.Vercel]: AppConnection.Vercel }; diff --git a/frontend/src/hooks/api/appConnections/enums.ts b/frontend/src/hooks/api/appConnections/enums.ts index ce8fea116..b0830459d 100644 --- a/frontend/src/hooks/api/appConnections/enums.ts +++ b/frontend/src/hooks/api/appConnections/enums.ts @@ -6,6 +6,7 @@ export enum AppConnection { AzureAppConfiguration = "azure-app-configuration", Databricks = "databricks", Humanitec = "humanitec", + TerraformCloud = "terraform-cloud", Vercel = "vercel", Postgres = "postgres", MsSql = "mssql", diff --git a/frontend/src/hooks/api/appConnections/terraform-cloud/index.ts b/frontend/src/hooks/api/appConnections/terraform-cloud/index.ts new file mode 100644 index 000000000..2c1906d36 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/terraform-cloud/index.ts @@ -0,0 +1,2 @@ +export * from "./queries"; +export * from "./types"; diff --git a/frontend/src/hooks/api/appConnections/terraform-cloud/queries.tsx b/frontend/src/hooks/api/appConnections/terraform-cloud/queries.tsx new file mode 100644 index 000000000..a5da22a0a --- /dev/null +++ b/frontend/src/hooks/api/appConnections/terraform-cloud/queries.tsx @@ -0,0 +1,37 @@ +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { appConnectionKeys } from "../queries"; +import { TTerraformCloudOrganization } from "./types"; + +const terraformCloudConnectionKeys = { + all: [...appConnectionKeys.all, "terraform-cloud"] as const, + listOrganizations: (connectionId: string) => + [...terraformCloudConnectionKeys.all, "organizations", connectionId] as const +}; + +export const useTerraformCloudConnectionListOrganizations = ( + connectionId: string, + options?: Omit< + UseQueryOptions< + TTerraformCloudOrganization[], + unknown, + TTerraformCloudOrganization[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: terraformCloudConnectionKeys.listOrganizations(connectionId), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/app-connections/terraform-cloud/${connectionId}/organizations` + ); + + return data; + }, + ...options + }); +}; diff --git a/frontend/src/hooks/api/appConnections/terraform-cloud/types.ts b/frontend/src/hooks/api/appConnections/terraform-cloud/types.ts new file mode 100644 index 000000000..7402976f5 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/terraform-cloud/types.ts @@ -0,0 +1,56 @@ +export type TTerraformCloudOrganization = { + name: string; + id: string; + variableSets: TTerraformCloudVariableSet[]; + workspaces: TTerraformCloudWorkspace[]; +}; + +export type TTerraformCloudVariableSet = { + id: string; + name: string; +}; + +export type TTerraformCloudWorkspace = { + id: string; + name: string; +}; + +export type TTerraformCloudConnectionOrganization = { + id: string; + name: string; + variableSets: TTerraformCloudConnectionVariableSet[]; + workspaces: TTerraformCloudConnectionWorkspace[]; +}; + +export type TTerraformCloudConnectionVariableSet = { + id: string; + name: string; + description: string; + global: boolean; +}; + +export type TTerraformCloudConnectionWorkspace = { + id: string; + name: string; +}; + +export enum TerraformCloudSyncScope { + VariableSet = "variable-set", + Workspace = "workspace" +} + +export enum TerraformCloudSyncCategory { + Environment = "env", + Terraform = "terraform" +} + +export const TERRAFORM_CLOUD_SYNC_SCOPES = { + [TerraformCloudSyncScope.VariableSet]: { + name: "Variable Set", + description: "Sync secrets to a specific variable set in Terraform Cloud." + }, + [TerraformCloudSyncScope.Workspace]: { + name: "Workspace", + description: "Sync secrets to a specific workspace in Terraform Cloud." + } +}; diff --git a/frontend/src/hooks/api/appConnections/types/app-options.ts b/frontend/src/hooks/api/appConnections/types/app-options.ts index 73d42c463..3bd172283 100644 --- a/frontend/src/hooks/api/appConnections/types/app-options.ts +++ b/frontend/src/hooks/api/appConnections/types/app-options.ts @@ -39,6 +39,10 @@ export type THumanitecConnectionOption = TAppConnectionOptionBase & { app: AppConnection.Humanitec; }; +export type TTerraformCloudConnectionOption = TAppConnectionOptionBase & { + app: AppConnection.TerraformCloud; +}; + export type TVercelConnectionOption = TAppConnectionOptionBase & { app: AppConnection.Vercel; }; @@ -63,6 +67,7 @@ export type TAppConnectionOption = | TAzureKeyVaultConnectionOption | TDatabricksConnectionOption | THumanitecConnectionOption + | TTerraformCloudConnectionOption | TVercelConnectionOption | TPostgresConnectionOption | TMsSqlConnectionOption @@ -76,6 +81,7 @@ export type TAppConnectionOptionMap = { [AppConnection.AzureAppConfiguration]: TAzureAppConfigurationConnectionOption; [AppConnection.Databricks]: TDatabricksConnectionOption; [AppConnection.Humanitec]: THumanitecConnectionOption; + [AppConnection.TerraformCloud]: TTerraformCloudConnectionOption; [AppConnection.Vercel]: TVercelConnectionOption; [AppConnection.Postgres]: TPostgresConnectionOption; [AppConnection.MsSql]: TMsSqlConnectionOption; diff --git a/frontend/src/hooks/api/appConnections/types/index.ts b/frontend/src/hooks/api/appConnections/types/index.ts index b8a529a18..eb1b71ebc 100644 --- a/frontend/src/hooks/api/appConnections/types/index.ts +++ b/frontend/src/hooks/api/appConnections/types/index.ts @@ -10,6 +10,7 @@ import { TGitHubConnection } from "./github-connection"; import { THumanitecConnection } from "./humanitec-connection"; import { TMsSqlConnection } from "./mssql-connection"; import { TPostgresConnection } from "./postgres-connection"; +import { TTerraformCloudConnection } from "./terraform-cloud-connection"; import { TVercelConnection } from "./vercel-connection"; export * from "./aws-connection"; @@ -22,6 +23,7 @@ export * from "./github-connection"; export * from "./humanitec-connection"; export * from "./mssql-connection"; export * from "./postgres-connection"; +export * from "./terraform-cloud-connection"; export * from "./vercel-connection"; export type TAppConnection = @@ -32,6 +34,7 @@ export type TAppConnection = | TAzureAppConfigurationConnection | TDatabricksConnection | THumanitecConnection + | TTerraformCloudConnection | TVercelConnection | TPostgresConnection | TMsSqlConnection @@ -70,6 +73,7 @@ export type TAppConnectionMap = { [AppConnection.AzureAppConfiguration]: TAzureAppConfigurationConnection; [AppConnection.Databricks]: TDatabricksConnection; [AppConnection.Humanitec]: THumanitecConnection; + [AppConnection.TerraformCloud]: TTerraformCloudConnection; [AppConnection.Vercel]: TVercelConnection; [AppConnection.Postgres]: TPostgresConnection; [AppConnection.MsSql]: TMsSqlConnection; diff --git a/frontend/src/hooks/api/appConnections/types/terraform-cloud-connection.ts b/frontend/src/hooks/api/appConnections/types/terraform-cloud-connection.ts new file mode 100644 index 000000000..ddc49d776 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/types/terraform-cloud-connection.ts @@ -0,0 +1,15 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection"; + +export enum TerraformCloudConnectionMethod { + ApiToken = "api-token" +} + +export type TTerraformCloudConnection = TRootAppConnection & { + app: AppConnection.TerraformCloud; +} & { + method: TerraformCloudConnectionMethod.ApiToken; + credentials: { + apiToken: string; + }; +}; diff --git a/frontend/src/hooks/api/secretSyncs/enums.ts b/frontend/src/hooks/api/secretSyncs/enums.ts index 9fa5f6c99..449ddf7e0 100644 --- a/frontend/src/hooks/api/secretSyncs/enums.ts +++ b/frontend/src/hooks/api/secretSyncs/enums.ts @@ -7,6 +7,7 @@ export enum SecretSync { AzureAppConfiguration = "azure-app-configuration", Databricks = "databricks", Humanitec = "humanitec", + TerraformCloud = "terraform-cloud", Camunda = "camunda", Vercel = "vercel" } diff --git a/frontend/src/hooks/api/secretSyncs/types/index.ts b/frontend/src/hooks/api/secretSyncs/types/index.ts index 207ab0118..c6e5f2762 100644 --- a/frontend/src/hooks/api/secretSyncs/types/index.ts +++ b/frontend/src/hooks/api/secretSyncs/types/index.ts @@ -10,6 +10,7 @@ import { TAzureKeyVaultSync } from "./azure-key-vault-sync"; import { TCamundaSync } from "./camunda-sync"; import { TGcpSync } from "./gcp-sync"; import { THumanitecSync } from "./humanitec-sync"; +import { TTerraformCloudSync } from "./terraform-cloud-sync"; import { TVercelSync } from "./vercel-sync"; export type TSecretSyncOption = { @@ -27,6 +28,7 @@ export type TSecretSync = | TAzureAppConfigurationSync | TDatabricksSync | THumanitecSync + | TTerraformCloudSync | TCamundaSync | TVercelSync; diff --git a/frontend/src/hooks/api/secretSyncs/types/terraform-cloud-sync.ts b/frontend/src/hooks/api/secretSyncs/types/terraform-cloud-sync.ts new file mode 100644 index 000000000..a8ab23aeb --- /dev/null +++ b/frontend/src/hooks/api/secretSyncs/types/terraform-cloud-sync.ts @@ -0,0 +1,34 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; +import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync"; + +import { TerraformCloudSyncCategory } from "../../appConnections/terraform-cloud"; + +export type TTerraformCloudSync = TRootSecretSync & { + destination: SecretSync.TerraformCloud; + destinationConfig: + | { + scope: TerraformCloudSyncScope.VariableSet; + org: string; + category: TerraformCloudSyncCategory; + variableSetId: string; + variableSetName: string; + } + | { + scope: TerraformCloudSyncScope.Workspace; + org: string; + category: TerraformCloudSyncCategory; + workspaceId: string; + workspaceName: string; + }; + connection: { + app: AppConnection.TerraformCloud; + name: string; + id: string; + }; +}; + +export enum TerraformCloudSyncScope { + VariableSet = "variable-set", + Workspace = "workspace" +} diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx index 59bdd581f..cd93ed5aa 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx @@ -19,6 +19,7 @@ import { GitHubConnectionForm } from "./GitHubConnectionForm"; import { HumanitecConnectionForm } from "./HumanitecConnectionForm"; import { MsSqlConnectionForm } from "./MsSqlConnectionForm"; import { PostgresConnectionForm } from "./PostgresConnectionForm"; +import { TerraformCloudConnectionForm } from "./TerraformCloudConnectionForm"; import { VercelConnectionForm } from "./VercelConnectionForm"; type FormProps = { @@ -72,6 +73,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => { return ; case AppConnection.Humanitec: return ; + case AppConnection.TerraformCloud: + return ; case AppConnection.Vercel: return ; case AppConnection.Postgres: @@ -130,6 +133,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => { return ; case AppConnection.Humanitec: return ; + case AppConnection.TerraformCloud: + return ; case AppConnection.Vercel: return ; case AppConnection.Postgres: diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/TerraformCloudConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/TerraformCloudConnectionForm.tsx new file mode 100644 index 000000000..97744c067 --- /dev/null +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/TerraformCloudConnectionForm.tsx @@ -0,0 +1,135 @@ +import { Controller, FormProvider, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { + Button, + FormControl, + ModalClose, + SecretInput, + Select, + SelectItem +} from "@app/components/v2"; +import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; +import { + TerraformCloudConnectionMethod, + TTerraformCloudConnection +} from "@app/hooks/api/appConnections"; +import { AppConnection } from "@app/hooks/api/appConnections/enums"; + +import { + genericAppConnectionFieldsSchema, + GenericAppConnectionsFields +} from "./GenericAppConnectionFields"; + +type Props = { + appConnection?: TTerraformCloudConnection; + onSubmit: (formData: FormData) => void; +}; + +const rootSchema = genericAppConnectionFieldsSchema.extend({ + app: z.literal(AppConnection.TerraformCloud) +}); + +const formSchema = z.discriminatedUnion("method", [ + rootSchema.extend({ + method: z.literal(TerraformCloudConnectionMethod.ApiToken), + credentials: z.object({ + apiToken: z.string().trim().min(1, "API Token required") + }) + }) +]); + +type FormData = z.infer; + +export const TerraformCloudConnectionForm = ({ appConnection, onSubmit }: Props) => { + const isUpdate = Boolean(appConnection); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: appConnection ?? { + app: AppConnection.TerraformCloud, + method: TerraformCloudConnectionMethod.ApiToken + } + }); + + const { + handleSubmit, + control, + formState: { isSubmitting, isDirty } + } = form; + + return ( + +
+ {!isUpdate && } + ( + + + + )} + /> + ( + + onChange(e.target.value)} + /> + + )} + /> +
+ + + + +
+ +
+ ); +}; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx index b8965b140..700bc5ce3 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx @@ -9,6 +9,7 @@ import { DatabricksSyncDestinationCol } from "./DatabricksSyncDestinationCol"; import { GcpSyncDestinationCol } from "./GcpSyncDestinationCol"; import { GitHubSyncDestinationCol } from "./GitHubSyncDestinationCol"; import { HumanitecSyncDestinationCol } from "./HumanitecSyncDestinationCol"; +import { TerraformCloudSyncDestinationCol } from "./TerraformCloudSyncDestinationCol"; import { VercelSyncDestinationCol } from "./VercelSyncDestinationCol"; type Props = { @@ -33,6 +34,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => { return ; case SecretSync.Humanitec: return ; + case SecretSync.TerraformCloud: + return ; case SecretSync.Camunda: return ; case SecretSync.Vercel: diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/TerraformCloudSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/TerraformCloudSyncDestinationCol.tsx new file mode 100644 index 000000000..71ddcc56a --- /dev/null +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/TerraformCloudSyncDestinationCol.tsx @@ -0,0 +1,14 @@ +import { TTerraformCloudSync } from "@app/hooks/api/secretSyncs/types/terraform-cloud-sync"; + +import { getSecretSyncDestinationColValues } from "../helpers"; +import { SecretSyncTableCell } from "../SecretSyncTableCell"; + +type Props = { + secretSync: TTerraformCloudSync; +}; + +export const TerraformCloudSyncDestinationCol = ({ secretSync }: Props) => { + const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync); + + return ; +}; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts index 1b457dc2b..13e921e4b 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts @@ -1,3 +1,4 @@ +import { TerraformCloudSyncScope } from "@app/hooks/api/appConnections/terraform-cloud"; import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs"; import { GitHubSyncScope, @@ -73,6 +74,14 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => { } secondaryText = `Organization - ${destinationConfig.org}`; break; + case SecretSync.TerraformCloud: + primaryText = destinationConfig.org; + if (destinationConfig.scope === TerraformCloudSyncScope.VariableSet) { + secondaryText = destinationConfig.variableSetName; + } else { + secondaryText = destinationConfig.workspaceName; + } + break; case SecretSync.Camunda: primaryText = destinationConfig.clusterName ?? destinationConfig.clusterUUID; secondaryText = "Cluster"; diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx index 9b8ff36a7..d2ef25975 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx @@ -19,6 +19,7 @@ import { AzureKeyVaultSyncDestinationSection } from "./AzureKeyVaultSyncDestinat import { CamundaSyncDestinationSection } from "./CamundaSyncDestinationSection"; import { GcpSyncDestinationSection } from "./GcpSyncDestinationSection"; import { HumanitecSyncDestinationSection } from "./HumanitecSyncDestinationSection"; +import { TerraformCloudSyncDestinationSection } from "./TerraformCloudSyncDestinationSection"; import { VercelSyncDestinationSection } from "./VercelSyncDestinationSection"; type Props = { @@ -59,6 +60,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }: case SecretSync.Humanitec: DestinationComponents = ; break; + case SecretSync.TerraformCloud: + DestinationComponents = ; + break; case SecretSync.Camunda: DestinationComponents = ; break; diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/TerraformCloudSyncDestinationSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/TerraformCloudSyncDestinationSection.tsx new file mode 100644 index 000000000..5cb902eda --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/TerraformCloudSyncDestinationSection.tsx @@ -0,0 +1,65 @@ +import { ReactNode } from "react"; + +import { GenericFieldLabel } from "@app/components/secret-syncs"; +import { TerraformCloudSyncCategory } from "@app/hooks/api/appConnections/terraform-cloud"; +import { + TerraformCloudSyncScope, + TTerraformCloudSync +} from "@app/hooks/api/secretSyncs/types/terraform-cloud-sync"; + +type Props = { + secretSync: TTerraformCloudSync; +}; + +export const TerraformCloudSyncDestinationSection = ({ secretSync }: Props) => { + const { destinationConfig } = secretSync; + + let Components: ReactNode; + switch (destinationConfig.scope) { + case TerraformCloudSyncScope.VariableSet: + Components = ( + <> + {destinationConfig.org} + + {destinationConfig.variableSetName} + + + {Object.keys(TerraformCloudSyncCategory).find( + (key) => + TerraformCloudSyncCategory[key as keyof typeof TerraformCloudSyncCategory] === + destinationConfig.category + )} + + + ); + break; + case TerraformCloudSyncScope.Workspace: + Components = ( + <> + {destinationConfig.org} + {destinationConfig.workspaceName} + + {Object.keys(TerraformCloudSyncCategory).find( + (key) => + TerraformCloudSyncCategory[key as keyof typeof TerraformCloudSyncCategory] === + destinationConfig.category + )} + + + ); + break; + default: + throw new Error( + `Unhandled Terraform Cloud Sync Destination Section Scope ${secretSync.destinationConfig.scope}` + ); + } + + return ( + <> + + {destinationConfig.scope.replace("-", " ")} + + {Components} + + ); +}; diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx index eb3dad61a..e37294ed8 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx @@ -48,6 +48,7 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) = case SecretSync.AzureAppConfiguration: case SecretSync.Databricks: case SecretSync.Humanitec: + case SecretSync.TerraformCloud: case SecretSync.Camunda: case SecretSync.Vercel: AdditionalSyncOptionsComponent = null;