diff --git a/backend/src/integrations/apps.ts b/backend/src/integrations/apps.ts index b2fec9292..cac83dca2 100644 --- a/backend/src/integrations/apps.ts +++ b/backend/src/integrations/apps.ts @@ -28,6 +28,8 @@ import { INTEGRATION_RENDER_API_URL, INTEGRATION_SUPABASE, INTEGRATION_SUPABASE_API_URL, + INTEGRATION_TERRAFORM_CLOUD, + INTEGRATION_TERRAFORM_CLOUD_API_URL, INTEGRATION_TRAVISCI, INTEGRATION_TRAVISCI_API_URL, INTEGRATION_VERCEL, @@ -124,6 +126,12 @@ const getApps = async ({ serverId: accessId }); break; + case INTEGRATION_TERRAFORM_CLOUD: + apps = await getAppsTerraformCloud({ + accessToken, + organizationName: accessId, + }); + break; case INTEGRATION_TRAVISCI: apps = await getAppsTravisCI({ accessToken, @@ -532,6 +540,39 @@ const getAppsTravisCI = async ({ accessToken }: { accessToken: string }) => { return apps; }; +/** + * Return list of projects for Terraform Cloud integration + * @param {Object} obj + * @param {String} obj.accessToken - access token for Terraform Cloud API + * @returns {Object[]} apps - names and ids of Terraform Cloud projects + * @returns {String} apps.name - name of Terraform Cloud projects + */ +const getAppsTerraformCloud = async ({ + accessToken, + organizationName +}: { + accessToken: string; + organizationName?: string; +}) => { + const res = ( + await standardRequest.get(`${INTEGRATION_TERRAFORM_CLOUD_API_URL}/api/v2/organizations/${organizationName}/projects`, { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json", + }, + }) + ).data.data; + + const apps = res?.map((a: any) => { + return { + name: a?.attributes.name, + }; + }); + + return apps; +}; + + /** * Return list of repositories for GitLab integration * @param {Object} obj diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index 5d8efa101..97893659d 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -38,6 +38,8 @@ import { INTEGRATION_RENDER_API_URL, INTEGRATION_SUPABASE, INTEGRATION_SUPABASE_API_URL, + INTEGRATION_TERRAFORM_CLOUD, + INTEGRATION_TERRAFORM_CLOUD_API_URL, INTEGRATION_TRAVISCI, INTEGRATION_TRAVISCI_API_URL, INTEGRATION_VERCEL, @@ -185,6 +187,14 @@ const syncSecrets = async ({ accessToken, }); break; + case INTEGRATION_TERRAFORM_CLOUD: + await syncSecretsTerraformCloud({ + integration, + secrets, + accessId, + accessToken, + }); + break; case INTEGRATION_HASHICORP_VAULT: await syncSecretsHashiCorpVault({ integration, @@ -1806,6 +1816,39 @@ const syncSecretsCheckly = async ({ } }; +/** + * Sync/push [secrets] to Terraform Cloud projects with id [integration.appId] + * @param {Object} obj + * @param {IIntegration} obj.integration - integration details + * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) + * @param {String} obj.accessToken - access token for Terraform Cloud integration + */ +const syncSecretsTerraformCloud = async ({ + integration, + secrets, + accessId, + accessToken, +}: { + integration: IIntegration; + secrets: any; + accessId: string | null; + accessToken: string; +}) => { + await standardRequest.put( + `${INTEGRATION_TERRAFORM_CLOUD_API_URL}/api/v2/organizations/${accessId}/${integration.app}`, + Object.keys(secrets).map((key) => ({ + key, + value: secrets[key], + })), + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json", + }, + } + ); +}; + /** * Sync/push [secrets] to HashiCorp Vault path * @param {Object} obj diff --git a/backend/src/models/integration.ts b/backend/src/models/integration.ts index c4021977d..77c9f85d2 100644 --- a/backend/src/models/integration.ts +++ b/backend/src/models/integration.ts @@ -16,6 +16,7 @@ import { INTEGRATION_RAILWAY, INTEGRATION_RENDER, INTEGRATION_SUPABASE, + INTEGRATION_TERRAFORM_CLOUD, INTEGRATION_TRAVISCI, INTEGRATION_VERCEL, } from "../variables"; @@ -53,6 +54,7 @@ export interface IIntegration { | "travisci" | "supabase" | "checkly" + | "terraform-cloud" | "hashicorp-vault" | "cloudflare-pages"; integrationAuth: Types.ObjectId; @@ -142,6 +144,7 @@ const integrationSchema = new Schema( INTEGRATION_TRAVISCI, INTEGRATION_SUPABASE, INTEGRATION_CHECKLY, + INTEGRATION_TERRAFORM_CLOUD, INTEGRATION_HASHICORP_VAULT, INTEGRATION_CLOUDFLARE_PAGES, ], diff --git a/backend/src/models/integrationAuth.ts b/backend/src/models/integrationAuth.ts index ed3dabadf..8c48ffaea 100644 --- a/backend/src/models/integrationAuth.ts +++ b/backend/src/models/integrationAuth.ts @@ -18,6 +18,7 @@ import { INTEGRATION_RAILWAY, INTEGRATION_RENDER, INTEGRATION_SUPABASE, + INTEGRATION_TERRAFORM_CLOUD, INTEGRATION_TRAVISCI, INTEGRATION_VERCEL } from "../variables"; @@ -25,7 +26,7 @@ import { export interface IIntegrationAuth extends Document { _id: Types.ObjectId; workspace: Types.ObjectId; - integration: "heroku" | "vercel" | "netlify" | "github" | "gitlab" | "render" | "railway" | "flyio" | "azure-key-vault" | "laravel-forge" | "circleci" | "travisci" | "supabase" | "aws-parameter-store" | "aws-secret-manager" | "checkly" | "cloudflare-pages"; + integration: "heroku" | "vercel" | "netlify" | "github" | "gitlab" | "render" | "railway" | "flyio" | "azure-key-vault" | "laravel-forge" | "circleci" | "travisci" | "supabase" | "aws-parameter-store" | "aws-secret-manager" | "terraform-cloud" | "checkly" | "cloudflare-pages"; teamId: string; accountId: string; url: string; @@ -69,6 +70,7 @@ const integrationAuthSchema = new Schema( INTEGRATION_LARAVELFORGE, INTEGRATION_TRAVISCI, INTEGRATION_SUPABASE, + INTEGRATION_TERRAFORM_CLOUD, INTEGRATION_HASHICORP_VAULT, INTEGRATION_CLOUDFLARE_PAGES, ], diff --git a/backend/src/variables/integration.ts b/backend/src/variables/integration.ts index 18eaeb049..687355949 100644 --- a/backend/src/variables/integration.ts +++ b/backend/src/variables/integration.ts @@ -24,6 +24,7 @@ export const INTEGRATION_CIRCLECI = "circleci"; export const INTEGRATION_TRAVISCI = "travisci"; export const INTEGRATION_SUPABASE = "supabase"; export const INTEGRATION_CHECKLY = "checkly"; +export const INTEGRATION_TERRAFORM_CLOUD = "terraform-cloud"; export const INTEGRATION_HASHICORP_VAULT = "hashicorp-vault"; export const INTEGRATION_CLOUDFLARE_PAGES = "cloudflare-pages"; export const INTEGRATION_SET = new Set([ @@ -40,6 +41,7 @@ export const INTEGRATION_SET = new Set([ INTEGRATION_TRAVISCI, INTEGRATION_SUPABASE, INTEGRATION_CHECKLY, + INTEGRATION_TERRAFORM_CLOUD, INTEGRATION_HASHICORP_VAULT, INTEGRATION_CLOUDFLARE_PAGES ]); @@ -70,6 +72,7 @@ export const INTEGRATION_TRAVISCI_API_URL = "https://api.travis-ci.com"; export const INTEGRATION_SUPABASE_API_URL = "https://api.supabase.com"; export const INTEGRATION_LARAVELFORGE_API_URL = "https://forge.laravel.com"; export const INTEGRATION_CHECKLY_API_URL = "https://api.checklyhq.com"; +export const INTEGRATION_TERRAFORM_CLOUD_API_URL = "https://app.terraform.io"; export const INTEGRATION_CLOUDFLARE_PAGES_API_URL = "https://api.cloudflare.com"; export const getIntegrationOptions = async () => { @@ -219,6 +222,15 @@ export const getIntegrationOptions = async () => { clientId: "", docsLink: "", }, + { + name: "Terraform Cloud", + slug: "terraform-cloud", + image: "Terraform Cloud.png", + isAvailable: true, + type: "pat", + cliendId: "", + docsLink: "", + }, { name: "HashiCorp Vault", slug: "hashicorp-vault", diff --git a/frontend/public/images/integrations/Terraform Cloud.png b/frontend/public/images/integrations/Terraform Cloud.png new file mode 100644 index 000000000..c0000e98c Binary files /dev/null and b/frontend/public/images/integrations/Terraform Cloud.png differ diff --git a/frontend/src/pages/integrations/terraform-cloud/authorize.tsx b/frontend/src/pages/integrations/terraform-cloud/authorize.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/pages/integrations/terraform-cloud/create.tsx b/frontend/src/pages/integrations/terraform-cloud/create.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx b/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx index fd82d1fa7..fc91a12a8 100644 --- a/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx +++ b/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx @@ -86,6 +86,9 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) => case "railway": link = `${window.location.origin}/integrations/railway/authorize`; break; + case "terraform-cloud": + link = `${window.location.origin}/integrations/terraform-cloud/authorize`; + break; case "hashicorp-vault": link = `${window.location.origin}/integrations/hashicorp-vault/authorize`; break;