Terraform Cloud Secret Sync Integration

This commit is contained in:
carlosmonastyrski
2025-04-04 09:54:53 -03:00
parent f00a54ed54
commit 4d41513abf
17 changed files with 212 additions and 113 deletions
+4 -3
View File
@@ -1791,9 +1791,10 @@ export const SecretSyncs = {
}, },
TERRAFORM_CLOUD: { TERRAFORM_CLOUD: {
org: "The ID of the Terraform Cloud org to sync secrets to.", org: "The ID of the Terraform Cloud org to sync secrets to.",
project: "The ID of the Terraform Cloud project to sync secrets to.", destinationName: "The name of the Terraform Cloud variable set / workspace to sync secrets to.",
workspace: "The ID of the Terraform Cloud 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." scope: "The Terraform Cloud scope that secrets should be synced to.",
category: "The Terraform Cloud category that secrets should be synced to."
} }
} }
}; };
@@ -6,7 +6,7 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums
import { import {
CreateTerraformCloudConnectionSchema, CreateTerraformCloudConnectionSchema,
SanitizedTerraformCloudConnectionSchema, SanitizedTerraformCloudConnectionSchema,
TerraformCloudOrgWithApps, TTerraformCloudOrganization,
UpdateTerraformCloudConnectionSchema UpdateTerraformCloudConnectionSchema
} from "@app/services/app-connection/terraform-cloud"; } from "@app/services/app-connection/terraform-cloud";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
@@ -38,10 +38,12 @@ export const registerTerraformCloudConnectionRouter = async (server: FastifyZodP
.object({ .object({
id: z.string(), id: z.string(),
name: z.string(), name: z.string(),
projects: z variableSets: z
.object({ .object({
id: z.string(), id: z.string(),
name: z.string() name: z.string(),
description: z.string().optional(),
global: z.boolean().optional()
}) })
.array(), .array(),
workspaces: z workspaces: z
@@ -58,7 +60,7 @@ export const registerTerraformCloudConnectionRouter = async (server: FastifyZodP
handler: async (req) => { handler: async (req) => {
const { connectionId } = req.params; const { connectionId } = req.params;
const organizations: TerraformCloudOrgWithApps[] = const organizations: TTerraformCloudOrganization[] =
await server.services.appConnection.terraformCloud.listOrganizations(connectionId, req.permission); await server.services.appConnection.terraformCloud.listOrganizations(connectionId, req.permission);
return organizations; return organizations;
@@ -38,6 +38,12 @@ import {
THumanitecConnectionInput, THumanitecConnectionInput,
TValidateHumanitecConnectionCredentials TValidateHumanitecConnectionCredentials
} from "./humanitec"; } from "./humanitec";
import {
TTerraformCloudConnection,
TTerraformCloudConnectionConfig,
TTerraformCloudConnectionInput,
TValidateTerraformCloudConnectionCredentials
} from "./terraform-cloud";
export type TAppConnection = { id: string } & ( export type TAppConnection = { id: string } & (
| TAwsConnection | TAwsConnection
@@ -47,6 +53,7 @@ export type TAppConnection = { id: string } & (
| TAzureAppConfigurationConnection | TAzureAppConfigurationConnection
| TDatabricksConnection | TDatabricksConnection
| THumanitecConnection | THumanitecConnection
| TTerraformCloudConnection
); );
export type TAppConnectionInput = { id: string } & ( export type TAppConnectionInput = { id: string } & (
@@ -57,6 +64,7 @@ export type TAppConnectionInput = { id: string } & (
| TAzureAppConfigurationConnectionInput | TAzureAppConfigurationConnectionInput
| TDatabricksConnectionInput | TDatabricksConnectionInput
| THumanitecConnectionInput | THumanitecConnectionInput
| TTerraformCloudConnectionInput
); );
export type TCreateAppConnectionDTO = Pick< export type TCreateAppConnectionDTO = Pick<
@@ -75,7 +83,8 @@ export type TAppConnectionConfig =
| TAzureKeyVaultConnectionConfig | TAzureKeyVaultConnectionConfig
| TAzureAppConfigurationConnectionConfig | TAzureAppConfigurationConnectionConfig
| TDatabricksConnectionConfig | TDatabricksConnectionConfig
| THumanitecConnectionConfig; | THumanitecConnectionConfig
| TTerraformCloudConnectionConfig;
export type TValidateAppConnectionCredentials = export type TValidateAppConnectionCredentials =
| TValidateAwsConnectionCredentials | TValidateAwsConnectionCredentials
@@ -84,7 +93,8 @@ export type TValidateAppConnectionCredentials =
| TValidateAzureKeyVaultConnectionCredentials | TValidateAzureKeyVaultConnectionCredentials
| TValidateAzureAppConfigurationConnectionCredentials | TValidateAzureAppConfigurationConnectionCredentials
| TValidateDatabricksConnectionCredentials | TValidateDatabricksConnectionCredentials
| TValidateHumanitecConnectionCredentials; | TValidateHumanitecConnectionCredentials
| TValidateTerraformCloudConnectionCredentials;
export type TListAwsConnectionKmsKeys = { export type TListAwsConnectionKmsKeys = {
connectionId: string; connectionId: string;
@@ -10,7 +10,7 @@ import {
TTerraformCloudConnection, TTerraformCloudConnection,
TTerraformCloudConnectionConfig, TTerraformCloudConnectionConfig,
TTerraformCloudOrganization, TTerraformCloudOrganization,
TTerraformCloudProject, TTerraformCloudVariableSet,
TTerraformCloudWorkspace TTerraformCloudWorkspace
} from "./terraform-cloud-connection-types"; } from "./terraform-cloud-connection-types";
@@ -81,12 +81,12 @@ export const listOrganizations = async (
} }
const orgEntities = orgsResponse.data.data; const orgEntities = orgsResponse.data.data;
const orgsWithProjectsAndWorkspaces: TTerraformCloudOrganization[] = []; const orgsWithVariableSetsAndWorkspaces: TTerraformCloudOrganization[] = [];
const projectPromises = orgEntities.map((org) => const variableSetPromises = orgEntities.map((org) =>
request request
.get<{ data: { id: string; attributes: { name: string } }[] }>( .get<{ data: { id: string; attributes: { name: string; description?: string; global?: boolean } }[] }>(
`${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/organizations/${org.id}/projects`, `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/organizations/${org.id}/varsets`,
{ {
headers: { headers: {
Authorization: `Bearer ${apiToken}`, Authorization: `Bearer ${apiToken}`,
@@ -111,19 +111,21 @@ export const listOrganizations = async (
.catch(() => ({ data: { data: [] } })) .catch(() => ({ data: { data: [] } }))
); );
const [projectResponses, workspaceResponses] = await Promise.all([ const [variableSetResponses, workspaceResponses] = await Promise.all([
Promise.all(projectPromises), Promise.all(variableSetPromises),
Promise.all(workspacePromises) Promise.all(workspacePromises)
]); ]);
for (let i = 0; i < orgEntities.length; i += 1) { for (let i = 0; i < orgEntities.length; i += 1) {
const org = orgEntities[i]; const org = orgEntities[i];
const projectsData = projectResponses[i].data?.data || []; const variableSetsData = variableSetResponses[i].data?.data || [];
const workspacesData = workspaceResponses[i].data?.data || []; const workspacesData = workspaceResponses[i].data?.data || [];
const projects: TTerraformCloudProject[] = projectsData.map((project) => ({ const variableSets: TTerraformCloudVariableSet[] = variableSetsData.map((varSet) => ({
id: project.id, id: varSet.id,
name: project.attributes.name name: varSet.attributes.name,
description: varSet.attributes.description,
global: varSet.attributes.global
})); }));
const workspaces: TTerraformCloudWorkspace[] = workspacesData.map((workspace) => ({ const workspaces: TTerraformCloudWorkspace[] = workspacesData.map((workspace) => ({
@@ -131,13 +133,13 @@ export const listOrganizations = async (
name: workspace.attributes.name name: workspace.attributes.name
})); }));
orgsWithProjectsAndWorkspaces.push({ orgsWithVariableSetsAndWorkspaces.push({
id: org.id, id: org.id,
name: org.attributes.name, name: org.attributes.name,
projects, variableSets,
workspaces workspaces
}); });
} }
return orgsWithProjectsAndWorkspaces; return orgsWithVariableSetsAndWorkspaces;
}; };
@@ -39,9 +39,11 @@ export type TerraformCloudOrgWithApps = TerraformCloudOrg & {
apps: TerraformCloudApp[]; apps: TerraformCloudApp[];
}; };
export type TTerraformCloudProject = { export type TTerraformCloudVariableSet = {
id: string; id: string;
name: string; name: string;
description?: string;
global?: boolean;
}; };
export type TTerraformCloudWorkspace = { export type TTerraformCloudWorkspace = {
@@ -52,15 +54,15 @@ export type TTerraformCloudWorkspace = {
export type TTerraformCloudOrganization = { export type TTerraformCloudOrganization = {
id: string; id: string;
name: string; name: string;
projects: TTerraformCloudProject[]; variableSets: TTerraformCloudVariableSet[];
workspaces: TTerraformCloudWorkspace[]; workspaces: TTerraformCloudWorkspace[];
}; };
export type TTerraformCloudConnectionOrganization = TTerraformCloudOrganization; export type TTerraformCloudConnectionOrganization = TTerraformCloudOrganization;
export type TTerraformCloudConnectionProject = TTerraformCloudProject; export type TTerraformCloudConnectionVariableSet = TTerraformCloudVariableSet;
export type TTerraformCloudConnectionWorkspace = TTerraformCloudWorkspace; export type TTerraformCloudConnectionWorkspace = TTerraformCloudWorkspace;
export enum TerraformCloudSyncScope { export enum TerraformCloudSyncScope {
Project = "project", VariableSet = "variableSet",
Workspace = "workspace" Workspace = "workspace"
} }
@@ -1,4 +1,9 @@
export enum TerraformCloudSyncScope { export enum TerraformCloudSyncScope {
Project = "project", VariableSet = "variable-set",
Workspace = "workspace" Workspace = "workspace"
} }
export enum TerraformCloudSyncCategory {
Environment = "env",
Terraform = "terraform"
}
@@ -3,9 +3,9 @@ import { request } from "@app/lib/config/request";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; 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 { 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 { TerraformCloudSyncScope } from "./terraform-cloud-sync-enums";
import { import {
TerraformCloudApiResponse, TerraformCloudApiResponse,
@@ -25,10 +25,14 @@ const getTerraformCloudVariables = async (
} = secretSync; } = secretSync;
let url; let url;
if (destinationConfig.scope === TerraformCloudSyncScope.Project) { let source: TerraformCloudVariable["source"];
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`;
source = "varset";
} else { } 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<TerraformCloudApiResponse<TerraformCloudApiVariable[]>>(url, { const response = await request.get<TerraformCloudApiResponse<TerraformCloudApiVariable[]>>(url, {
@@ -49,7 +53,7 @@ const getTerraformCloudVariables = async (
sensitive: variable.attributes.sensitive, sensitive: variable.attributes.sensitive,
description: variable.attributes.description || "", description: variable.attributes.description || "",
category: variable.attributes.category, category: variable.attributes.category,
source: destinationConfig.scope === TerraformCloudSyncScope.Project ? "project" : "workspace" source
})); }));
return variables; return variables;
@@ -68,10 +72,11 @@ const deleteVariable = async (
try { try {
let url; 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 { } 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, { await request.delete(url, {
@@ -104,10 +109,11 @@ const createVariable = async (
} = secretSync; } = secretSync;
let url; 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 { } 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( await request.post(
@@ -119,9 +125,8 @@ const createVariable = async (
key, key,
value: secretMap[key].value, value: secretMap[key].value,
description: secretMap[key].comment || "", description: secretMap[key].comment || "",
category: "env", category: secretSync.destinationConfig.category,
sensitive: true, sensitive: true
hcl: false
} }
} }
}, },
@@ -156,10 +161,11 @@ const updateVariable = async (
} = secretSync; } = secretSync;
let url; 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 { } 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( await request.patch(
@@ -170,7 +176,8 @@ const updateVariable = async (
id: variable.id, id: variable.id,
attributes: { attributes: {
value: secretMap[variable.key].value, value: secretMap[variable.key].value,
description: secretMap[variable.key].comment || "" description: secretMap[variable.key].comment || "",
category: secretSync.destinationConfig.category
} }
} }
}, },
@@ -9,24 +9,39 @@ import {
GenericUpdateSecretSyncFieldsSchema GenericUpdateSecretSyncFieldsSchema
} from "@app/services/secret-sync/secret-sync-schemas"; } from "@app/services/secret-sync/secret-sync-schemas";
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; 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", [ const TerraformCloudSyncDestinationConfigSchema = z.discriminatedUnion("scope", [
z.object({ 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), org: z.string().min(1, "Org ID is required").describe(SecretSyncs.DESTINATION_CONFIG.TERRAFORM_CLOUD.org),
project: z destinationName: z
.string() .string()
.min(1, "Project ID is required") .min(1, "Variable set name is required")
.describe(SecretSyncs.DESTINATION_CONFIG.TERRAFORM_CLOUD.project) .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({ z.object({
scope: z.literal(TerraformCloudSyncScope.Workspace).describe(SecretSyncs.DESTINATION_CONFIG.TERRAFORM_CLOUD.scope), 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), 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() .string()
.min(1, "Workspace ID is required") .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)
}) })
]); ]);
@@ -60,7 +60,7 @@ export type TerraformCloudVariable = {
sensitive: boolean; sensitive: boolean;
description: string; description: string;
category: "terraform" | "env"; category: "terraform" | "env";
source: "project" | "workspace"; source: "varset" | "workspace";
}; };
export type TerraformCloudApiResponse<T> = { export type TerraformCloudApiResponse<T> = {
@@ -83,8 +83,3 @@ export type TerraformCloudApiResponse<T> = {
}; };
}; };
}; };
export enum TerraformCloudSyncScope {
Project = "project",
Workspace = "workspace"
}
@@ -7,9 +7,10 @@ import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/Se
import { FilterableSelect, FormControl, Select, SelectItem, Tooltip } from "@app/components/v2"; import { FilterableSelect, FormControl, Select, SelectItem, Tooltip } from "@app/components/v2";
import { import {
TERRAFORM_CLOUD_SYNC_SCOPES, TERRAFORM_CLOUD_SYNC_SCOPES,
TerraformCloudSyncCategory,
TerraformCloudSyncScope, TerraformCloudSyncScope,
TTerraformCloudConnectionOrganization, TTerraformCloudConnectionOrganization,
TTerraformCloudConnectionProject, TTerraformCloudConnectionVariableSet,
TTerraformCloudConnectionWorkspace, TTerraformCloudConnectionWorkspace,
useTerraformCloudConnectionListOrganizations useTerraformCloudConnectionListOrganizations
} from "@app/hooks/api/appConnections/terraform-cloud"; } from "@app/hooks/api/appConnections/terraform-cloud";
@@ -32,7 +33,7 @@ export const TerraformCloudSyncFields = () => {
}); });
const selectedOrg = organizations?.find((org) => org.id === currentOrg); const selectedOrg = organizations?.find((org) => org.id === currentOrg);
const projects = selectedOrg?.projects || []; const variableSets = selectedOrg?.variableSets || [];
const workspaces = selectedOrg?.workspaces || []; const workspaces = selectedOrg?.workspaces || [];
return ( return (
@@ -40,8 +41,8 @@ export const TerraformCloudSyncFields = () => {
<SecretSyncConnectionField <SecretSyncConnectionField
onChange={() => { onChange={() => {
setValue("destinationConfig.org", ""); setValue("destinationConfig.org", "");
setValue("destinationConfig.project", ""); setValue("destinationConfig.destinationId", "");
setValue("destinationConfig.workspace", ""); setValue("destinationConfig.destinationName", "");
}} }}
/> />
<Controller <Controller
@@ -61,8 +62,8 @@ export const TerraformCloudSyncFields = () => {
onChange( onChange(
(option as SingleValue<TTerraformCloudConnectionOrganization>)?.id ?? null (option as SingleValue<TTerraformCloudConnectionOrganization>)?.id ?? null
); );
setValue("destinationConfig.project", ""); setValue("destinationConfig.destinationId", "");
setValue("destinationConfig.workspace", ""); setValue("destinationConfig.destinationName", "");
}} }}
options={organizations} options={organizations}
placeholder="Select an organization..." placeholder="Select an organization..."
@@ -72,10 +73,38 @@ export const TerraformCloudSyncFields = () => {
</FormControl> </FormControl>
)} )}
/> />
<Controller
name="destinationConfig.category"
control={control}
defaultValue={TerraformCloudSyncCategory.Environment}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Category"
>
<Select
value={value}
onValueChange={onChange}
className="w-full border border-mineshaft-500 capitalize"
position="popper"
defaultValue={TerraformCloudSyncCategory.Environment}
placeholder="Select category..."
dropdownContainerClassName="max-w-none"
>
{Object.values(TerraformCloudSyncCategory).map((category) => (
<SelectItem className="capitalize" value={category} key={category}>
{category.replace("-", " ")}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller <Controller
name="destinationConfig.scope" name="destinationConfig.scope"
control={control} control={control}
defaultValue={TerraformCloudSyncScope.Project} defaultValue={TerraformCloudSyncScope.VariableSet}
render={({ field: { value, onChange }, fieldState: { error } }) => ( render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl <FormControl
errorText={error?.message} errorText={error?.message}
@@ -106,12 +135,8 @@ export const TerraformCloudSyncFields = () => {
value={value} value={value}
onValueChange={(val) => { onValueChange={(val) => {
onChange(val); onChange(val);
// Reset either project or workspace based on which scope was selected setValue("destinationConfig.destinationId", "");
if (val === TerraformCloudSyncScope.Project) { setValue("destinationConfig.destinationName", "");
setValue("destinationConfig.workspace", "");
} else {
setValue("destinationConfig.project", "");
}
}} }}
className="w-full border border-mineshaft-500 capitalize" className="w-full border border-mineshaft-500 capitalize"
position="popper" position="popper"
@@ -127,22 +152,22 @@ export const TerraformCloudSyncFields = () => {
</FormControl> </FormControl>
)} )}
/> />
{currentScope === TerraformCloudSyncScope.Project && ( {currentScope === TerraformCloudSyncScope.VariableSet && (
<Controller <Controller
name="destinationConfig.project" name="destinationConfig.destinationId"
control={control} control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => ( render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl <FormControl
isError={Boolean(error)} isError={Boolean(error)}
errorText={error?.message} errorText={error?.message}
label="Project" label="Variable Set"
helperText={ helperText={
<Tooltip <Tooltip
className="max-w-md" className="max-w-md"
content="Ensure that the project exists in the selected organization and the service account used on this connection has write permissions for the specified project." content="Ensure that the variable set exists in the selected organization and the service account used on this connection has write permissions for the specified variable set."
> >
<div> <div>
<span>Don&#39;t see the project you&#39;re looking for?</span>{" "} <span>Don&#39;t see the variable set you&#39;re looking for?</span>{" "}
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" /> <FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
</div> </div>
</Tooltip> </Tooltip>
@@ -152,12 +177,20 @@ export const TerraformCloudSyncFields = () => {
menuPlacement="top" menuPlacement="top"
isLoading={isOrganizationsPending && Boolean(connectionId) && Boolean(currentOrg)} isLoading={isOrganizationsPending && Boolean(connectionId) && Boolean(currentOrg)}
isDisabled={!connectionId || !currentOrg} isDisabled={!connectionId || !currentOrg}
value={projects.find((project) => project.id === value) ?? null} value={variableSets.find((variableSet) => variableSet.id === value) ?? null}
onChange={(option) => { onChange={(option) => {
onChange((option as SingleValue<TTerraformCloudConnectionProject>)?.id ?? null); const selectedOption =
option as SingleValue<TTerraformCloudConnectionVariableSet>;
onChange(selectedOption?.id ?? null);
if (selectedOption) {
setValue("destinationConfig.destinationName", selectedOption.name);
} else {
setValue("destinationConfig.destinationName", "");
}
}} }}
options={projects} options={variableSets}
placeholder="Select a project..." placeholder="Select a variable set..."
getOptionLabel={(option) => option.name} getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id.toString()} getOptionValue={(option) => option.id.toString()}
/> />
@@ -167,7 +200,7 @@ export const TerraformCloudSyncFields = () => {
)} )}
{currentScope === TerraformCloudSyncScope.Workspace && ( {currentScope === TerraformCloudSyncScope.Workspace && (
<Controller <Controller
name="destinationConfig.workspace" name="destinationConfig.destinationId"
control={control} control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => ( render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl <FormControl
@@ -192,7 +225,14 @@ export const TerraformCloudSyncFields = () => {
isDisabled={!connectionId || !currentOrg} isDisabled={!connectionId || !currentOrg}
value={workspaces.find((workspace) => workspace.id === value) ?? null} value={workspaces.find((workspace) => workspace.id === value) ?? null}
onChange={(option) => { onChange={(option) => {
onChange((option as SingleValue<TTerraformCloudConnectionWorkspace>)?.id ?? null); const selectedOption = option as SingleValue<TTerraformCloudConnectionWorkspace>;
onChange(selectedOption?.id ?? null);
if (selectedOption) {
setValue("destinationConfig.destinationName", selectedOption.name);
} else {
setValue("destinationConfig.destinationName", "");
}
}} }}
options={workspaces} options={workspaces}
placeholder="Select a workspace..." placeholder="Select a workspace..."
@@ -8,19 +8,20 @@ import { SecretSync } from "@app/hooks/api/secretSyncs";
export const TerraformCloudSyncReviewFields = () => { export const TerraformCloudSyncReviewFields = () => {
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.TerraformCloud }>(); const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.TerraformCloud }>();
const orgId = watch("destinationConfig.org"); const orgId = watch("destinationConfig.org");
const projectId = watch("destinationConfig.project"); const destinationName = watch("destinationConfig.destinationName");
const workspaceId = watch("destinationConfig.workspace");
const scope = watch("destinationConfig.scope"); const scope = watch("destinationConfig.scope");
const category = watch("destinationConfig.category");
return ( return (
<> <>
<SecretSyncLabel label="Organization">{orgId}</SecretSyncLabel> <SecretSyncLabel label="Organization">{orgId}</SecretSyncLabel>
{scope === TerraformCloudSyncScope.Project && ( {scope === TerraformCloudSyncScope.VariableSet && (
<SecretSyncLabel label="Project">{projectId}</SecretSyncLabel> <SecretSyncLabel label="Variable Set">{destinationName}</SecretSyncLabel>
)} )}
{scope === TerraformCloudSyncScope.Workspace && ( {scope === TerraformCloudSyncScope.Workspace && (
<SecretSyncLabel label="Workspace">{workspaceId}</SecretSyncLabel> <SecretSyncLabel label="Workspace">{destinationName}</SecretSyncLabel>
)} )}
<SecretSyncLabel label="Category">{category}</SecretSyncLabel>
</> </>
); );
}; };
@@ -1,7 +1,10 @@
import { z } from "zod"; import { z } from "zod";
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema"; 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"; import { SecretSync } from "@app/hooks/api/secretSyncs";
export const TerraformCloudSyncDestinationSchema = BaseSecretSyncSchema().merge( export const TerraformCloudSyncDestinationSchema = BaseSecretSyncSchema().merge(
@@ -9,14 +12,18 @@ export const TerraformCloudSyncDestinationSchema = BaseSecretSyncSchema().merge(
destination: z.literal(SecretSync.TerraformCloud), destination: z.literal(SecretSync.TerraformCloud),
destinationConfig: z.discriminatedUnion("scope", [ destinationConfig: z.discriminatedUnion("scope", [
z.object({ z.object({
scope: z.literal(TerraformCloudSyncScope.Project), scope: z.literal(TerraformCloudSyncScope.VariableSet),
org: z.string().trim().min(1, "Organization required"), 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({ z.object({
scope: z.literal(TerraformCloudSyncScope.Workspace), scope: z.literal(TerraformCloudSyncScope.Workspace),
org: z.string().trim().min(1, "Organization required"), 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)
}) })
]) ])
}) })
+2 -1
View File
@@ -8,7 +8,8 @@ import {
AzureKeyVaultConnectionMethod, AzureKeyVaultConnectionMethod,
GcpConnectionMethod, GcpConnectionMethod,
GitHubConnectionMethod, GitHubConnectionMethod,
TAppConnection TAppConnection,
TerraformCloudConnectionMethod
} from "@app/hooks/api/appConnections/types"; } from "@app/hooks/api/appConnections/types";
import { DatabricksConnectionMethod } from "@app/hooks/api/appConnections/types/databricks-connection"; import { DatabricksConnectionMethod } from "@app/hooks/api/appConnections/types/databricks-connection";
import { HumanitecConnectionMethod } from "@app/hooks/api/appConnections/types/humanitec-connection"; import { HumanitecConnectionMethod } from "@app/hooks/api/appConnections/types/humanitec-connection";
@@ -1,11 +1,11 @@
export type TTerraformCloudOrganization = { export type TTerraformCloudOrganization = {
name: string; name: string;
id: string; id: string;
projects: TTerraformCloudProject[]; variableSets: TTerraformCloudVariableSet[];
workspaces: TTerraformCloudWorkspace[]; workspaces: TTerraformCloudWorkspace[];
}; };
export type TTerraformCloudProject = { export type TTerraformCloudVariableSet = {
id: string; id: string;
name: string; name: string;
}; };
@@ -18,13 +18,15 @@ export type TTerraformCloudWorkspace = {
export type TTerraformCloudConnectionOrganization = { export type TTerraformCloudConnectionOrganization = {
id: string; id: string;
name: string; name: string;
projects: TTerraformCloudConnectionProject[]; variableSets: TTerraformCloudConnectionVariableSet[];
workspaces: TTerraformCloudConnectionWorkspace[]; workspaces: TTerraformCloudConnectionWorkspace[];
}; };
export type TTerraformCloudConnectionProject = { export type TTerraformCloudConnectionVariableSet = {
id: string; id: string;
name: string; name: string;
description: string;
global: boolean;
}; };
export type TTerraformCloudConnectionWorkspace = { export type TTerraformCloudConnectionWorkspace = {
@@ -33,14 +35,19 @@ export type TTerraformCloudConnectionWorkspace = {
}; };
export enum TerraformCloudSyncScope { export enum TerraformCloudSyncScope {
Project = "project", VariableSet = "variable-set",
Workspace = "workspace" Workspace = "workspace"
} }
export enum TerraformCloudSyncCategory {
Environment = "env",
Terraform = "terraform"
}
export const TERRAFORM_CLOUD_SYNC_SCOPES = { export const TERRAFORM_CLOUD_SYNC_SCOPES = {
[TerraformCloudSyncScope.Project]: { [TerraformCloudSyncScope.VariableSet]: {
name: "Project", name: "Variable Set",
description: "Sync secrets to a specific project in Terraform Cloud." description: "Sync secrets to a specific variable set in Terraform Cloud."
}, },
[TerraformCloudSyncScope.Workspace]: { [TerraformCloudSyncScope.Workspace]: {
name: "Workspace", name: "Workspace",
@@ -2,18 +2,24 @@ import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { SecretSync } from "@app/hooks/api/secretSyncs"; import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync"; import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
import { TerraformCloudSyncCategory } from "../../appConnections/terraform-cloud";
export type TTerraformCloudSync = TRootSecretSync & { export type TTerraformCloudSync = TRootSecretSync & {
destination: SecretSync.TerraformCloud; destination: SecretSync.TerraformCloud;
destinationConfig: destinationConfig:
| { | {
scope: TerraformCloudSyncScope.Project; scope: TerraformCloudSyncScope.VariableSet;
org: string; org: string;
project: string; category: TerraformCloudSyncCategory;
destinationId: string;
destinationName: string;
} }
| { | {
scope: TerraformCloudSyncScope.Workspace; scope: TerraformCloudSyncScope.Workspace;
org: string; org: string;
workspace: string; category: TerraformCloudSyncCategory;
destinationId: string;
destinationName: string;
}; };
connection: { connection: {
app: AppConnection.TerraformCloud; app: AppConnection.TerraformCloud;
@@ -23,6 +29,6 @@ export type TTerraformCloudSync = TRootSecretSync & {
}; };
export enum TerraformCloudSyncScope { export enum TerraformCloudSyncScope {
Project = "project", VariableSet = "variable-set",
Workspace = "workspace" Workspace = "workspace"
} }
@@ -1,4 +1,3 @@
import { TerraformCloudSyncScope } from "@app/hooks/api/appConnections/terraform-cloud";
import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs"; import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs";
import { import {
GitHubSyncScope, GitHubSyncScope,
@@ -76,10 +75,7 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => {
break; break;
case SecretSync.TerraformCloud: case SecretSync.TerraformCloud:
primaryText = destinationConfig.org; primaryText = destinationConfig.org;
secondaryText = secondaryText = destinationConfig.destinationName;
destinationConfig.scope === TerraformCloudSyncScope.Project
? destinationConfig.project
: destinationConfig.workspace;
break; break;
default: default:
throw new Error(`Unhandled Destination Col Values ${destination}`); throw new Error(`Unhandled Destination Col Values ${destination}`);
@@ -15,11 +15,13 @@ export const TerraformCloudSyncDestinationSection = ({ secretSync }: Props) => {
let Components: ReactNode; let Components: ReactNode;
switch (destinationConfig.scope) { switch (destinationConfig.scope) {
case TerraformCloudSyncScope.Project: case TerraformCloudSyncScope.VariableSet:
Components = ( Components = (
<> <>
<SecretSyncLabel label="Organization">{destinationConfig.org}</SecretSyncLabel> <SecretSyncLabel label="Organization">{destinationConfig.org}</SecretSyncLabel>
<SecretSyncLabel label="Project">{destinationConfig.project}</SecretSyncLabel> <SecretSyncLabel label="Variable Set">
{destinationConfig.destinationName}
</SecretSyncLabel>
</> </>
); );
break; break;
@@ -27,7 +29,7 @@ export const TerraformCloudSyncDestinationSection = ({ secretSync }: Props) => {
Components = ( Components = (
<> <>
<SecretSyncLabel label="Organization">{destinationConfig.org}</SecretSyncLabel> <SecretSyncLabel label="Organization">{destinationConfig.org}</SecretSyncLabel>
<SecretSyncLabel label="Workspace">{destinationConfig.workspace}</SecretSyncLabel> <SecretSyncLabel label="Workspace">{destinationConfig.destinationName}</SecretSyncLabel>
</> </>
); );
break; break;