From 4d41513abf09db7317ddc6eaec2d4ab0c4757f24 Mon Sep 17 00:00:00 2001 From: carlosmonastyrski Date: Fri, 4 Apr 2025 09:54:53 -0300 Subject: [PATCH] Terraform Cloud Secret Sync Integration --- backend/src/lib/api-docs/constants.ts | 7 +- .../terraform-cloud-router.ts | 10 ++- .../app-connection/app-connection-types.ts | 14 ++- .../terraform-cloud-connection-fns.ts | 30 ++++--- .../terraform-cloud-connection-types.ts | 10 ++- .../terraform-cloud-sync-enums.ts | 7 +- .../terraform-cloud-sync-fns.ts | 43 +++++---- .../terraform-cloud-sync-schemas.ts | 29 ++++-- .../terraform-cloud-sync-types.ts | 7 +- .../TerraformCloudSyncFields.tsx | 88 ++++++++++++++----- .../TerraformCloudSyncReviewFields.tsx | 11 +-- .../terraform-cloud-destination-schema.ts | 15 +++- frontend/src/helpers/appConnections.ts | 3 +- .../appConnections/terraform-cloud/types.ts | 23 +++-- .../secretSyncs/types/terraform-cloud-sync.ts | 14 ++- .../SecretSyncTable/helpers/index.ts | 6 +- .../TerraformCloudSyncDestinationCol.tsx | 8 +- 17 files changed, 212 insertions(+), 113 deletions(-) diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index a4ceb6079..6635296e4 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1791,9 +1791,10 @@ export const SecretSyncs = { }, TERRAFORM_CLOUD: { org: "The ID of the Terraform Cloud org to sync secrets to.", - project: "The ID of the Terraform Cloud project to sync secrets to.", - workspace: "The ID of the Terraform Cloud workspace to sync secrets to.", - scope: "The Terraform Cloud scope that secrets should be synced to." + destinationName: "The name of the Terraform Cloud variable set / workspace to sync secrets to.", + destinationId: "The ID of the Terraform Cloud variable set / 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." } } }; diff --git a/backend/src/server/routes/v1/app-connection-routers/terraform-cloud-router.ts b/backend/src/server/routes/v1/app-connection-routers/terraform-cloud-router.ts index 66af8886f..6e0cab34a 100644 --- a/backend/src/server/routes/v1/app-connection-routers/terraform-cloud-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/terraform-cloud-router.ts @@ -6,7 +6,7 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums import { CreateTerraformCloudConnectionSchema, SanitizedTerraformCloudConnectionSchema, - TerraformCloudOrgWithApps, + TTerraformCloudOrganization, UpdateTerraformCloudConnectionSchema } from "@app/services/app-connection/terraform-cloud"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -38,10 +38,12 @@ export const registerTerraformCloudConnectionRouter = async (server: FastifyZodP .object({ id: z.string(), name: z.string(), - projects: z + variableSets: z .object({ id: z.string(), - name: z.string() + name: z.string(), + description: z.string().optional(), + global: z.boolean().optional() }) .array(), workspaces: z @@ -58,7 +60,7 @@ export const registerTerraformCloudConnectionRouter = async (server: FastifyZodP handler: async (req) => { const { connectionId } = req.params; - const organizations: TerraformCloudOrgWithApps[] = + const organizations: TTerraformCloudOrganization[] = await server.services.appConnection.terraformCloud.listOrganizations(connectionId, req.permission); return organizations; diff --git a/backend/src/services/app-connection/app-connection-types.ts b/backend/src/services/app-connection/app-connection-types.ts index 7051ecb11..c2f6d1d70 100644 --- a/backend/src/services/app-connection/app-connection-types.ts +++ b/backend/src/services/app-connection/app-connection-types.ts @@ -38,6 +38,12 @@ import { THumanitecConnectionInput, TValidateHumanitecConnectionCredentials } from "./humanitec"; +import { + TTerraformCloudConnection, + TTerraformCloudConnectionConfig, + TTerraformCloudConnectionInput, + TValidateTerraformCloudConnectionCredentials +} from "./terraform-cloud"; export type TAppConnection = { id: string } & ( | TAwsConnection @@ -47,6 +53,7 @@ export type TAppConnection = { id: string } & ( | TAzureAppConfigurationConnection | TDatabricksConnection | THumanitecConnection + | TTerraformCloudConnection ); export type TAppConnectionInput = { id: string } & ( @@ -57,6 +64,7 @@ export type TAppConnectionInput = { id: string } & ( | TAzureAppConfigurationConnectionInput | TDatabricksConnectionInput | THumanitecConnectionInput + | TTerraformCloudConnectionInput ); export type TCreateAppConnectionDTO = Pick< @@ -75,7 +83,8 @@ export type TAppConnectionConfig = | TAzureKeyVaultConnectionConfig | TAzureAppConfigurationConnectionConfig | TDatabricksConnectionConfig - | THumanitecConnectionConfig; + | THumanitecConnectionConfig + | TTerraformCloudConnectionConfig; export type TValidateAppConnectionCredentials = | TValidateAwsConnectionCredentials @@ -84,7 +93,8 @@ export type TValidateAppConnectionCredentials = | TValidateAzureKeyVaultConnectionCredentials | TValidateAzureAppConfigurationConnectionCredentials | TValidateDatabricksConnectionCredentials - | TValidateHumanitecConnectionCredentials; + | TValidateHumanitecConnectionCredentials + | TValidateTerraformCloudConnectionCredentials; export type TListAwsConnectionKmsKeys = { connectionId: string; diff --git a/backend/src/services/app-connection/terraform-cloud/terraform-cloud-connection-fns.ts b/backend/src/services/app-connection/terraform-cloud/terraform-cloud-connection-fns.ts index e043bc2d0..25b6c6a72 100644 --- a/backend/src/services/app-connection/terraform-cloud/terraform-cloud-connection-fns.ts +++ b/backend/src/services/app-connection/terraform-cloud/terraform-cloud-connection-fns.ts @@ -10,7 +10,7 @@ import { TTerraformCloudConnection, TTerraformCloudConnectionConfig, TTerraformCloudOrganization, - TTerraformCloudProject, + TTerraformCloudVariableSet, TTerraformCloudWorkspace } from "./terraform-cloud-connection-types"; @@ -81,12 +81,12 @@ export const listOrganizations = async ( } const orgEntities = orgsResponse.data.data; - const orgsWithProjectsAndWorkspaces: TTerraformCloudOrganization[] = []; + const orgsWithVariableSetsAndWorkspaces: TTerraformCloudOrganization[] = []; - const projectPromises = orgEntities.map((org) => + const variableSetPromises = orgEntities.map((org) => request - .get<{ data: { id: string; attributes: { name: string } }[] }>( - `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/organizations/${org.id}/projects`, + .get<{ data: { id: string; attributes: { name: string; description?: string; global?: boolean } }[] }>( + `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/organizations/${org.id}/varsets`, { headers: { Authorization: `Bearer ${apiToken}`, @@ -111,19 +111,21 @@ export const listOrganizations = async ( .catch(() => ({ data: { data: [] } })) ); - const [projectResponses, workspaceResponses] = await Promise.all([ - Promise.all(projectPromises), + const [variableSetResponses, workspaceResponses] = await Promise.all([ + Promise.all(variableSetPromises), Promise.all(workspacePromises) ]); for (let i = 0; i < orgEntities.length; i += 1) { const org = orgEntities[i]; - const projectsData = projectResponses[i].data?.data || []; + const variableSetsData = variableSetResponses[i].data?.data || []; const workspacesData = workspaceResponses[i].data?.data || []; - const projects: TTerraformCloudProject[] = projectsData.map((project) => ({ - id: project.id, - name: project.attributes.name + 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) => ({ @@ -131,13 +133,13 @@ export const listOrganizations = async ( name: workspace.attributes.name })); - orgsWithProjectsAndWorkspaces.push({ + orgsWithVariableSetsAndWorkspaces.push({ id: org.id, name: org.attributes.name, - projects, + variableSets, workspaces }); } - return orgsWithProjectsAndWorkspaces; + return orgsWithVariableSetsAndWorkspaces; }; 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 index 624bcc385..a0f17a457 100644 --- 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 @@ -39,9 +39,11 @@ export type TerraformCloudOrgWithApps = TerraformCloudOrg & { apps: TerraformCloudApp[]; }; -export type TTerraformCloudProject = { +export type TTerraformCloudVariableSet = { id: string; name: string; + description?: string; + global?: boolean; }; export type TTerraformCloudWorkspace = { @@ -52,15 +54,15 @@ export type TTerraformCloudWorkspace = { export type TTerraformCloudOrganization = { id: string; name: string; - projects: TTerraformCloudProject[]; + variableSets: TTerraformCloudVariableSet[]; workspaces: TTerraformCloudWorkspace[]; }; export type TTerraformCloudConnectionOrganization = TTerraformCloudOrganization; -export type TTerraformCloudConnectionProject = TTerraformCloudProject; +export type TTerraformCloudConnectionVariableSet = TTerraformCloudVariableSet; export type TTerraformCloudConnectionWorkspace = TTerraformCloudWorkspace; export enum TerraformCloudSyncScope { - Project = "project", + VariableSet = "variableSet", Workspace = "workspace" } 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 index e994047c0..cfd1daf2c 100644 --- 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 @@ -1,4 +1,9 @@ export enum TerraformCloudSyncScope { - Project = "project", + 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 index 21d4bb87f..00b0aae9c 100644 --- 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 @@ -3,9 +3,9 @@ import { request } from "@app/lib/config/request"; import { logger } from "@app/lib/logger"; import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; -import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps"; 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, @@ -25,10 +25,14 @@ const getTerraformCloudVariables = async ( } = secretSync; let url; - if (destinationConfig.scope === TerraformCloudSyncScope.Project) { - url = `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/organizations/${destinationConfig.org}/projects/${destinationConfig.project}/vars`; + let source: TerraformCloudVariable["source"]; + + if (destinationConfig.scope === TerraformCloudSyncScope.VariableSet) { + url = `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/varsets/${destinationConfig.destinationId}/relationships/vars`; + source = "varset"; } else { - url = `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/organizations/${destinationConfig.org}/workspaces/${destinationConfig.workspace}/vars`; + url = `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${destinationConfig.destinationId}/vars`; + source = "workspace"; } const response = await request.get>(url, { @@ -49,7 +53,7 @@ const getTerraformCloudVariables = async ( sensitive: variable.attributes.sensitive, description: variable.attributes.description || "", category: variable.attributes.category, - source: destinationConfig.scope === TerraformCloudSyncScope.Project ? "project" : "workspace" + source })); return variables; @@ -68,10 +72,11 @@ const deleteVariable = async ( try { let url; - if (destinationConfig.scope === TerraformCloudSyncScope.Project) { - url = `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/organizations/${destinationConfig.org}/projects/${destinationConfig.project}/vars/${variable.id}`; + + if (destinationConfig.scope === TerraformCloudSyncScope.VariableSet) { + url = `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/varsets/${destinationConfig.destinationId}/relationships/vars/${variable.id}`; } else { - url = `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/organizations/${destinationConfig.org}/workspaces/${destinationConfig.workspace}/vars/${variable.id}`; + url = `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${destinationConfig.destinationId}/vars/${variable.id}`; } await request.delete(url, { @@ -104,10 +109,11 @@ const createVariable = async ( } = secretSync; let url; - if (destinationConfig.scope === TerraformCloudSyncScope.Project) { - url = `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/organizations/${destinationConfig.org}/projects/${destinationConfig.project}/vars`; + + if (destinationConfig.scope === TerraformCloudSyncScope.VariableSet) { + url = `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/varsets/${destinationConfig.destinationId}/relationships/vars`; } else { - url = `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/organizations/${destinationConfig.org}/workspaces/${destinationConfig.workspace}/vars`; + url = `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${destinationConfig.destinationId}/vars`; } await request.post( @@ -119,9 +125,8 @@ const createVariable = async ( key, value: secretMap[key].value, description: secretMap[key].comment || "", - category: "env", - sensitive: true, - hcl: false + category: secretSync.destinationConfig.category, + sensitive: true } } }, @@ -156,10 +161,11 @@ const updateVariable = async ( } = secretSync; let url; - if (destinationConfig.scope === TerraformCloudSyncScope.Project) { - url = `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/organizations/${destinationConfig.org}/projects/${destinationConfig.project}/vars/${variable.id}`; + + if (destinationConfig.scope === TerraformCloudSyncScope.VariableSet) { + url = `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/varsets/${destinationConfig.destinationId}/relationships/vars/${variable.id}`; } else { - url = `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/organizations/${destinationConfig.org}/workspaces/${destinationConfig.workspace}/vars/${variable.id}`; + url = `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${destinationConfig.destinationId}/vars/${variable.id}`; } await request.patch( @@ -170,7 +176,8 @@ const updateVariable = async ( id: variable.id, attributes: { value: secretMap[variable.key].value, - description: secretMap[variable.key].comment || "" + description: secretMap[variable.key].comment || "", + category: secretSync.destinationConfig.category } } }, 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 index 71713d2b0..f19a942cd 100644 --- 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 @@ -9,24 +9,39 @@ import { GenericUpdateSecretSyncFieldsSchema } from "@app/services/secret-sync/secret-sync-schemas"; import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; -import { TerraformCloudSyncScope } from "@app/services/secret-sync/terraform-cloud/terraform-cloud-sync-enums"; +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.Project).describe(SecretSyncs.DESTINATION_CONFIG.TERRAFORM_CLOUD.scope), + 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), - project: z + destinationName: z .string() - .min(1, "Project ID is required") - .describe(SecretSyncs.DESTINATION_CONFIG.TERRAFORM_CLOUD.project) + .min(1, "Variable set name is required") + .describe(SecretSyncs.DESTINATION_CONFIG.TERRAFORM_CLOUD.destinationName), + destinationId: z + .string() + .min(1, "Variable set ID is required") + .describe(SecretSyncs.DESTINATION_CONFIG.TERRAFORM_CLOUD.destinationId), + 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), - workspace: z + destinationName: z + .string() + .min(1, "Workspace name is required") + .describe(SecretSyncs.DESTINATION_CONFIG.TERRAFORM_CLOUD.destinationName), + destinationId: z .string() .min(1, "Workspace ID is required") - .describe(SecretSyncs.DESTINATION_CONFIG.TERRAFORM_CLOUD.workspace) + .describe(SecretSyncs.DESTINATION_CONFIG.TERRAFORM_CLOUD.destinationId), + category: z.nativeEnum(TerraformCloudSyncCategory).describe(SecretSyncs.DESTINATION_CONFIG.TERRAFORM_CLOUD.category) }) ]); 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 index bb9cd2f43..ccbd32352 100644 --- 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 @@ -60,7 +60,7 @@ export type TerraformCloudVariable = { sensitive: boolean; description: string; category: "terraform" | "env"; - source: "project" | "workspace"; + source: "varset" | "workspace"; }; export type TerraformCloudApiResponse = { @@ -83,8 +83,3 @@ export type TerraformCloudApiResponse = { }; }; }; - -export enum TerraformCloudSyncScope { - Project = "project", - Workspace = "workspace" -} diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/TerraformCloudSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/TerraformCloudSyncFields.tsx index 7f73dbca3..4c05dde7a 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/TerraformCloudSyncFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/TerraformCloudSyncFields.tsx @@ -7,9 +7,10 @@ import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/Se import { FilterableSelect, FormControl, Select, SelectItem, Tooltip } from "@app/components/v2"; import { TERRAFORM_CLOUD_SYNC_SCOPES, + TerraformCloudSyncCategory, TerraformCloudSyncScope, TTerraformCloudConnectionOrganization, - TTerraformCloudConnectionProject, + TTerraformCloudConnectionVariableSet, TTerraformCloudConnectionWorkspace, useTerraformCloudConnectionListOrganizations } from "@app/hooks/api/appConnections/terraform-cloud"; @@ -32,7 +33,7 @@ export const TerraformCloudSyncFields = () => { }); const selectedOrg = organizations?.find((org) => org.id === currentOrg); - const projects = selectedOrg?.projects || []; + const variableSets = selectedOrg?.variableSets || []; const workspaces = selectedOrg?.workspaces || []; return ( @@ -40,8 +41,8 @@ export const TerraformCloudSyncFields = () => { { setValue("destinationConfig.org", ""); - setValue("destinationConfig.project", ""); - setValue("destinationConfig.workspace", ""); + setValue("destinationConfig.destinationId", ""); + setValue("destinationConfig.destinationName", ""); }} /> { onChange( (option as SingleValue)?.id ?? null ); - setValue("destinationConfig.project", ""); - setValue("destinationConfig.workspace", ""); + setValue("destinationConfig.destinationId", ""); + setValue("destinationConfig.destinationName", ""); }} options={organizations} placeholder="Select an organization..." @@ -72,10 +73,38 @@ export const TerraformCloudSyncFields = () => { )} /> + ( + + + + )} + /> ( { value={value} onValueChange={(val) => { onChange(val); - // Reset either project or workspace based on which scope was selected - if (val === TerraformCloudSyncScope.Project) { - setValue("destinationConfig.workspace", ""); - } else { - setValue("destinationConfig.project", ""); - } + setValue("destinationConfig.destinationId", ""); + setValue("destinationConfig.destinationName", ""); }} className="w-full border border-mineshaft-500 capitalize" position="popper" @@ -127,22 +152,22 @@ export const TerraformCloudSyncFields = () => { )} /> - {currentScope === TerraformCloudSyncScope.Project && ( + {currentScope === TerraformCloudSyncScope.VariableSet && ( (
- Don't see the project you're looking for?{" "} + Don't see the variable set you're looking for?{" "}
@@ -152,12 +177,20 @@ export const TerraformCloudSyncFields = () => { menuPlacement="top" isLoading={isOrganizationsPending && Boolean(connectionId) && Boolean(currentOrg)} isDisabled={!connectionId || !currentOrg} - value={projects.find((project) => project.id === value) ?? null} + value={variableSets.find((variableSet) => variableSet.id === value) ?? null} onChange={(option) => { - onChange((option as SingleValue)?.id ?? null); + const selectedOption = + option as SingleValue; + onChange(selectedOption?.id ?? null); + + if (selectedOption) { + setValue("destinationConfig.destinationName", selectedOption.name); + } else { + setValue("destinationConfig.destinationName", ""); + } }} - options={projects} - placeholder="Select a project..." + options={variableSets} + placeholder="Select a variable set..." getOptionLabel={(option) => option.name} getOptionValue={(option) => option.id.toString()} /> @@ -167,7 +200,7 @@ export const TerraformCloudSyncFields = () => { )} {currentScope === TerraformCloudSyncScope.Workspace && ( ( { isDisabled={!connectionId || !currentOrg} value={workspaces.find((workspace) => workspace.id === value) ?? null} onChange={(option) => { - onChange((option as SingleValue)?.id ?? null); + const selectedOption = option as SingleValue; + onChange(selectedOption?.id ?? null); + + if (selectedOption) { + setValue("destinationConfig.destinationName", selectedOption.name); + } else { + setValue("destinationConfig.destinationName", ""); + } }} options={workspaces} placeholder="Select a workspace..." diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/TerraformCloudSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/TerraformCloudSyncReviewFields.tsx index 6ffe03c47..61b1b68e0 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/TerraformCloudSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/TerraformCloudSyncReviewFields.tsx @@ -8,19 +8,20 @@ import { SecretSync } from "@app/hooks/api/secretSyncs"; export const TerraformCloudSyncReviewFields = () => { const { watch } = useFormContext(); const orgId = watch("destinationConfig.org"); - const projectId = watch("destinationConfig.project"); - const workspaceId = watch("destinationConfig.workspace"); + const destinationName = watch("destinationConfig.destinationName"); const scope = watch("destinationConfig.scope"); + const category = watch("destinationConfig.category"); return ( <> {orgId} - {scope === TerraformCloudSyncScope.Project && ( - {projectId} + {scope === TerraformCloudSyncScope.VariableSet && ( + {destinationName} )} {scope === TerraformCloudSyncScope.Workspace && ( - {workspaceId} + {destinationName} )} + {category} ); }; 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 index d75ea643a..c7cd945a6 100644 --- 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 @@ -1,7 +1,10 @@ import { z } from "zod"; import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema"; -import { TerraformCloudSyncScope } from "@app/hooks/api/appConnections/terraform-cloud"; +import { + TerraformCloudSyncCategory, + TerraformCloudSyncScope +} from "@app/hooks/api/appConnections/terraform-cloud"; import { SecretSync } from "@app/hooks/api/secretSyncs"; export const TerraformCloudSyncDestinationSchema = BaseSecretSyncSchema().merge( @@ -9,14 +12,18 @@ export const TerraformCloudSyncDestinationSchema = BaseSecretSyncSchema().merge( destination: z.literal(SecretSync.TerraformCloud), destinationConfig: z.discriminatedUnion("scope", [ z.object({ - scope: z.literal(TerraformCloudSyncScope.Project), + scope: z.literal(TerraformCloudSyncScope.VariableSet), org: z.string().trim().min(1, "Organization required"), - project: z.string().trim().min(1, "Project required") + destinationId: z.string().trim().min(1, "Variable set id required"), + destinationName: 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"), - workspace: z.string().trim().min(1, "Workspace required") + destinationId: z.string().trim().min(1, "Workspace id required"), + destinationName: 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 8c75b55c2..8be1df34e 100644 --- a/frontend/src/helpers/appConnections.ts +++ b/frontend/src/helpers/appConnections.ts @@ -8,7 +8,8 @@ import { AzureKeyVaultConnectionMethod, GcpConnectionMethod, GitHubConnectionMethod, - TAppConnection + TAppConnection, + TerraformCloudConnectionMethod } from "@app/hooks/api/appConnections/types"; import { DatabricksConnectionMethod } from "@app/hooks/api/appConnections/types/databricks-connection"; import { HumanitecConnectionMethod } from "@app/hooks/api/appConnections/types/humanitec-connection"; diff --git a/frontend/src/hooks/api/appConnections/terraform-cloud/types.ts b/frontend/src/hooks/api/appConnections/terraform-cloud/types.ts index 9b80d9a05..7402976f5 100644 --- a/frontend/src/hooks/api/appConnections/terraform-cloud/types.ts +++ b/frontend/src/hooks/api/appConnections/terraform-cloud/types.ts @@ -1,11 +1,11 @@ export type TTerraformCloudOrganization = { name: string; id: string; - projects: TTerraformCloudProject[]; + variableSets: TTerraformCloudVariableSet[]; workspaces: TTerraformCloudWorkspace[]; }; -export type TTerraformCloudProject = { +export type TTerraformCloudVariableSet = { id: string; name: string; }; @@ -18,13 +18,15 @@ export type TTerraformCloudWorkspace = { export type TTerraformCloudConnectionOrganization = { id: string; name: string; - projects: TTerraformCloudConnectionProject[]; + variableSets: TTerraformCloudConnectionVariableSet[]; workspaces: TTerraformCloudConnectionWorkspace[]; }; -export type TTerraformCloudConnectionProject = { +export type TTerraformCloudConnectionVariableSet = { id: string; name: string; + description: string; + global: boolean; }; export type TTerraformCloudConnectionWorkspace = { @@ -33,14 +35,19 @@ export type TTerraformCloudConnectionWorkspace = { }; export enum TerraformCloudSyncScope { - Project = "project", + VariableSet = "variable-set", Workspace = "workspace" } +export enum TerraformCloudSyncCategory { + Environment = "env", + Terraform = "terraform" +} + export const TERRAFORM_CLOUD_SYNC_SCOPES = { - [TerraformCloudSyncScope.Project]: { - name: "Project", - description: "Sync secrets to a specific project in Terraform Cloud." + [TerraformCloudSyncScope.VariableSet]: { + name: "Variable Set", + description: "Sync secrets to a specific variable set in Terraform Cloud." }, [TerraformCloudSyncScope.Workspace]: { name: "Workspace", diff --git a/frontend/src/hooks/api/secretSyncs/types/terraform-cloud-sync.ts b/frontend/src/hooks/api/secretSyncs/types/terraform-cloud-sync.ts index 9da8a848a..ffa224133 100644 --- a/frontend/src/hooks/api/secretSyncs/types/terraform-cloud-sync.ts +++ b/frontend/src/hooks/api/secretSyncs/types/terraform-cloud-sync.ts @@ -2,18 +2,24 @@ 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.Project; + scope: TerraformCloudSyncScope.VariableSet; org: string; - project: string; + category: TerraformCloudSyncCategory; + destinationId: string; + destinationName: string; } | { scope: TerraformCloudSyncScope.Workspace; org: string; - workspace: string; + category: TerraformCloudSyncCategory; + destinationId: string; + destinationName: string; }; connection: { app: AppConnection.TerraformCloud; @@ -23,6 +29,6 @@ export type TTerraformCloudSync = TRootSecretSync & { }; export enum TerraformCloudSyncScope { - Project = "project", + VariableSet = "variable-set", Workspace = "workspace" } 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 550474386..6c0fef9cb 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,4 +1,3 @@ -import { TerraformCloudSyncScope } from "@app/hooks/api/appConnections/terraform-cloud"; import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs"; import { GitHubSyncScope, @@ -76,10 +75,7 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => { break; case SecretSync.TerraformCloud: primaryText = destinationConfig.org; - secondaryText = - destinationConfig.scope === TerraformCloudSyncScope.Project - ? destinationConfig.project - : destinationConfig.workspace; + secondaryText = destinationConfig.destinationName; break; default: throw new Error(`Unhandled Destination Col Values ${destination}`); diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/TerraformCloudSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/TerraformCloudSyncDestinationCol.tsx index a6157c2b2..401561457 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/TerraformCloudSyncDestinationCol.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/TerraformCloudSyncDestinationCol.tsx @@ -15,11 +15,13 @@ export const TerraformCloudSyncDestinationSection = ({ secretSync }: Props) => { let Components: ReactNode; switch (destinationConfig.scope) { - case TerraformCloudSyncScope.Project: + case TerraformCloudSyncScope.VariableSet: Components = ( <> {destinationConfig.org} - {destinationConfig.project} + + {destinationConfig.destinationName} + ); break; @@ -27,7 +29,7 @@ export const TerraformCloudSyncDestinationSection = ({ secretSync }: Props) => { Components = ( <> {destinationConfig.org} - {destinationConfig.workspace} + {destinationConfig.destinationName} ); break;