diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 42cd4d902..01d769eac 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1694,6 +1694,9 @@ export const AppConnections = { sslEnabled: "Whether or not to use SSL when connecting to the database.", 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." } } }; 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 e0d743318..017766bae 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 @@ -64,62 +64,52 @@ export const listOrganizations = async ( credentials: { apiToken } } = appConnection; - const orgsResponse = await request.get<{ data: { id: string; attributes: { name: string } }[] }>( - `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/organizations`, - { - headers: { - Authorization: `Bearer ${apiToken}`, - "Content-Type": "application/vnd.api+json" - } + 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` ); - if (!orgsResponse.data?.data) { - throw new InternalServerError({ - message: "Failed to get organizations: Response was empty" - }); - } - - const orgEntities = orgsResponse.data.data; const orgsWithVariableSetsAndWorkspaces: TTerraformCloudOrganization[] = []; const variableSetPromises = orgEntities.map((org) => - request - .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}`, - "Content-Type": "application/vnd.api+json" - } - } - ) - .catch(() => ({ data: { data: [] } })) + 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) => - request - .get<{ data: { id: string; attributes: { name: string } }[] }>( - `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/organizations/${org.id}/workspaces`, - { - headers: { - Authorization: `Bearer ${apiToken}`, - "Content-Type": "application/vnd.api+json" - } - } - ) - .catch(() => ({ data: { data: [] } })) + fetchAllPages<{ id: string; attributes: { name: string } }>( + `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/organizations/${org.id}/workspaces` + ).catch(() => []) ); - const [variableSetResponses, workspaceResponses] = await Promise.all([ + 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 = variableSetResponses[i].data?.data || []; - const workspacesData = workspaceResponses[i].data?.data || []; + const variableSetsData = variableSetResults[i]; + const workspacesData = workspaceResults[i]; const variableSets: TTerraformCloudVariableSet[] = variableSetsData.map((varSet) => ({ id: varSet.id, 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 4f7de8880..cabcbb146 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 @@ -25,21 +25,6 @@ export type TTerraformCloudConnectionConfig = DiscriminativePick< orgId: string; }; -export type TerraformCloudOrg = { - id: string; - name: string; -}; - -export type TerraformCloudApp = { - name: string; - id: string; - envs: { name: string; id: string }[]; -}; - -export type TerraformCloudOrgWithApps = TerraformCloudOrg & { - apps: TerraformCloudApp[]; -}; - export type TTerraformCloudVariableSet = { id: string; name: string; @@ -58,12 +43,3 @@ export type TTerraformCloudOrganization = { variableSets: TTerraformCloudVariableSet[]; workspaces: TTerraformCloudWorkspace[]; }; - -export type TTerraformCloudConnectionOrganization = TTerraformCloudOrganization; -export type TTerraformCloudConnectionVariableSet = TTerraformCloudVariableSet; -export type TTerraformCloudConnectionWorkspace = TTerraformCloudWorkspace; - -export enum TerraformCloudSyncScope { - VariableSet = "variableSet", - Workspace = "workspace" -} 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 00b0aae9c..a40682be5 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 @@ -1,6 +1,5 @@ /* eslint-disable no-await-in-loop */ 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 { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; @@ -13,6 +12,7 @@ import { TerraformCloudVariable, TTerraformCloudSyncWithCredentials } from "./terraform-cloud-sync-types"; +import { AxiosResponse } from "axios"; const getTerraformCloudVariables = async ( secretSync: TTerraformCloudSyncWithCredentials @@ -24,7 +24,7 @@ const getTerraformCloudVariables = async ( } } = secretSync; - let url; + let url: string; let source: TerraformCloudVariable["source"]; if (destinationConfig.scope === TerraformCloudSyncScope.VariableSet) { @@ -35,18 +35,35 @@ const getTerraformCloudVariables = async ( source = "workspace"; } - const response = await request.get>(url, { - headers: { - Authorization: `Bearer ${apiToken}`, - "Content-Type": "application/vnd.api+json" + 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; } - }); - if (!response.data || !response.data.data) { - return []; - } + return results; + }; - const variables: TerraformCloudVariable[] = response.data.data.map((variable: TerraformCloudApiVariable) => ({ + const allVariableData = await fetchAllPages(); + + const variables: TerraformCloudVariable[] = allVariableData.map((variable) => ({ id: variable.id, key: variable.attributes.key, value: variable.attributes.value || "", @@ -85,8 +102,6 @@ const deleteVariable = async ( "Content-Type": "application/vnd.api+json" } }); - - logger.info(`Deleted variable ${variable.key} from Terraform Cloud ${destinationConfig.scope}`); } catch (error) { throw new SecretSyncError({ error, @@ -137,8 +152,6 @@ const createVariable = async ( } } ); - - logger.info(`Created variable ${key} in Terraform Cloud ${destinationConfig.scope}`); } catch (error) { throw new SecretSyncError({ error, @@ -188,8 +201,6 @@ const updateVariable = async ( } } ); - - logger.info(`Updated variable ${variable.key} in Terraform Cloud ${destinationConfig.scope}`); } catch (error) { throw new SecretSyncError({ error, 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 ccbd32352..f68db0d51 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 @@ -18,14 +18,6 @@ export type TTerraformCloudSyncWithCredentials = TTerraformCloudSync & { connection: TTerraformCloudConnection; }; -export type TerraformCloudSecret = { - description: string; - is_secret: boolean; - key: string; - source: "project" | "workspace"; - value: string; -}; - export type TerraformCloudApiVariable = { id: string; type: string; diff --git a/docs/api-reference/endpoints/app-connections/terraform-cloud/create.mdx b/docs/api-reference/endpoints/app-connections/terraform-cloud/create.mdx index 406fb9877..ad7d4a5d1 100644 --- a/docs/api-reference/endpoints/app-connections/terraform-cloud/create.mdx +++ b/docs/api-reference/endpoints/app-connections/terraform-cloud/create.mdx @@ -1,4 +1,9 @@ --- title: "Create" openapi: "POST /api/v1/app-connections/terraform-cloud" ---- \ No newline at end of file +--- + + + 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/update.mdx b/docs/api-reference/endpoints/app-connections/terraform-cloud/update.mdx index 488148158..b8f526a88 100644 --- a/docs/api-reference/endpoints/app-connections/terraform-cloud/update.mdx +++ b/docs/api-reference/endpoints/app-connections/terraform-cloud/update.mdx @@ -1,4 +1,9 @@ --- title: "Update" openapi: "PATCH /api/v1/app-connections/terraform-cloud/{connectionId}" ---- \ No newline at end of file +--- + + + 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 index 491889e16..f683d67fe 100644 --- a/docs/api-reference/endpoints/secret-syncs/terraform-cloud/create.mdx +++ b/docs/api-reference/endpoints/secret-syncs/terraform-cloud/create.mdx @@ -1,4 +1,4 @@ --- title: "Create" -openapi: "POST /api/v1/secret-syncs/terraform-cloud" +openapi: "POST /api/v1/secret-syncs/humanitec" --- diff --git a/docs/integrations/app-connections/terraform-cloud.mdx b/docs/integrations/app-connections/terraform-cloud.mdx index b4adb7b98..02deb22cc 100644 --- a/docs/integrations/app-connections/terraform-cloud.mdx +++ b/docs/integrations/app-connections/terraform-cloud.mdx @@ -34,7 +34,7 @@ Infisical supports connecting to Terraform Cloud using a service user. ![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 the Terraform Cloud Connection modal, here you will need to provide the API Token generated in the previous step. + 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) diff --git a/docs/integrations/secret-syncs/terraform-cloud.mdx b/docs/integrations/secret-syncs/terraform-cloud.mdx index 99f7e23a2..5de92ac23 100644 --- a/docs/integrations/secret-syncs/terraform-cloud.mdx +++ b/docs/integrations/secret-syncs/terraform-cloud.mdx @@ -94,7 +94,7 @@ description: "Learn how to configure a Terraform Cloud Sync for Infisical." }, "destinationConfig": { "scope": "variable-set", - "destinationId": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + "destinationId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "destinationName": "my-variable-set", "org": "my-organization-id", "category": "env" diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx index 4b615473c..7e310c00a 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx @@ -17,7 +17,6 @@ export const SecretSyncDestinationFields = () => { const { watch } = useFormContext(); const destination = watch("destination"); - console.log(destination); switch (destination) { case SecretSync.AWSParameterStore: diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/TerraformCloudSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/TerraformCloudSyncFields.tsx index 4c05dde7a..de97bdba8 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/TerraformCloudSyncFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/TerraformCloudSyncFields.tsx @@ -1,10 +1,8 @@ import { Controller, useFormContext, useWatch } from "react-hook-form"; import { SingleValue } from "react-select"; -import { faCircleInfo } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField"; -import { FilterableSelect, FormControl, Select, SelectItem, Tooltip } from "@app/components/v2"; +import { FilterableSelect, FormControl, Select, SelectItem } from "@app/components/v2"; import { TERRAFORM_CLOUD_SYNC_SCOPES, TerraformCloudSyncCategory, @@ -82,6 +80,28 @@ export const TerraformCloudSyncFields = () => { errorText={error?.message} isError={Boolean(error?.message)} label="Category" + tooltipClassName="max-w-lg py-3" + tooltipText={ +
+
    +
  • +

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

    +
  • +
  • +

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

    +
  • +
+
+ } > @@ -157,22 +177,7 @@ export const TerraformCloudSyncFields = () => { name="destinationConfig.destinationId" control={control} render={({ field: { value, onChange }, fieldState: { error } }) => ( - -
- Don't see the variable set you're looking for?{" "} - -
- - } - > + { name="destinationConfig.destinationId" control={control} render={({ field: { value, onChange }, fieldState: { error } }) => ( - -
- Don't see the workspace you're looking for?{" "} - -
- - } - > + ; break; case SecretSync.TerraformCloud: - DestinationComponents = ; + DestinationComponents = ; break; default: throw new Error(`Unhandled Destination Section components: ${destination}`);