From 3e975dc4f05ed6d4875e3f04b0bd607c6e9145f3 Mon Sep 17 00:00:00 2001 From: Andrew Atimapre Date: Sat, 8 Jul 2023 00:07:38 +0100 Subject: [PATCH] Terraform Cloud integration --- backend/src/integrations/apps.ts | 41 +++++++++++++++++ backend/src/integrations/sync.ts | 43 ++++++++++++++++++ backend/src/models/integration.ts | 3 ++ backend/src/models/integrationAuth.ts | 4 +- backend/src/variables/integration.ts | 12 +++++ .../images/integrations/Terraform Cloud.png | Bin 0 -> 6797 bytes .../terraform-cloud/authorize.tsx | 0 .../integrations/terraform-cloud/create.tsx | 0 .../IntegrationPage.utils.tsx | 3 ++ 9 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 frontend/public/images/integrations/Terraform Cloud.png create mode 100644 frontend/src/pages/integrations/terraform-cloud/authorize.tsx create mode 100644 frontend/src/pages/integrations/terraform-cloud/create.tsx 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 0000000000000000000000000000000000000000..c0000e98c8ed3573ab8bfb524b05a1945df9eeb8 GIT binary patch literal 6797 zcmeHMYgAL$w%(YsRoE)^neC%Qi-cN%kf>;our+FZpy5>8ibWeF61DhB5w#jk6tNtm zrpBY7qybSLii)Teu8Jrc6G0jtTB!lh3+d#(A+ zx#nK$o8MffM$P!5n^%7?f*`t0nLOc3g6L#nf4Y!(GygD|gkN2kPmcb9AiR6AKOV&1 z3@$G?%h{+ZtZF*Y#cA?EAi{?sV8kaQScX zuV~~HHM6y}_@zox-*MO1(72&vX~Ur5Up8`xvTxVs_gHvial8D~`L?$&&jc&;Wgf)c zK+c#W<>#yo+1YE4&3oJ4a9B3emqTc@8C%QW`TR2Q&YRZi1b2&|HH07nMfwv(dyl+o zn>X;?Z_66FR|#Sf*D|Jt&msKcc#oQ=n-T&zgzgGc)mR^O18;Q~m>_fXUvH_aOi3$H3P-XZ=39|IKOGP^)3lKs z7Q#&_=CC=uF`rl49}vp}Iq6@s5YuSOh#7#0QSdgw;3ux-`EL9c=9rk*Po7tV>{w&_iV|3M&w;fbl2{wbsA(d8>@SA zXoq$2uYWYIecyg(&xyJ{M4eo$78+-4Sog`rrdvL*A|lnYX_ComBI+Oib)HM-9o^ld zqvf&u7dKB+6PYXPF1@*-_xLn_$-!t+C`dk$^|pC~Y{raFs?Kbh*tCgIg&HO8g5(~h zRBS6R`R?V>gHt^Pnb#9Q7}#9P&pBPW!9e)w;wMDY;1xFqi0O6LCmH zMm7ED8bg|K()S(|1#P<2Y4QXUb#kIIraOn89BHEJrYmDUN%hp@kVOqLt|*}tJic3z zxmtX`Wy)|3`vz66LQx8m+paa_=+$BF*fj6pT@4}CcF!pb$Kw{4yCgFR|&^bfsK zJ#G7uUHd+;WoaE>8kT4-s9?xBN$;hlGJk%>F_$%pZw%SDb2gFuJYPaL9T3G2AcbYv zpn03C(&kPcP6XBPr56&-t_uuV436^JFUpbi^wu;x4TFCW$GPKqoC5GoNi{9%o$6WU zc3Jfv#t^AU?=dP@mhw@eS%-uahO33T3ChYeN}(}24Y2{a#uu^;wY11T)su;ES#8-{ zjW3417yxVlD{l#E*!-=46)K@iDtb{O9=wh!rK#+kxTvh66Ll0@}Y@l z`&EYI?vT(MtJ2<18BUmUWjWE}xT*Lce%Z2|L3kW;oR~b#6nQ<-yvxFnmQ9lMLk`_% z>6be3g58S0=ZbO?Ng=b_W!?3KGBOar?KS+F9~nbVVYj#B%KWE^;}#D+P6TX`q$fCZ z`RQ6JYM@%^bm$goFLla{bSowu6y*#BM7_&OO;tv21;k1lzc$Pmazv>2IJ93Dze*f8 zhku;N+aXCm;?UjO1&Cm^@cU}Id?FU;b{a}{R&g7YJkAKIsq$NKT=8BY2fFQMr|w=n zkoUQ)+c4P;iR-S-THYpvHU4ZKE8L>j;!VHnXaVH zVA|^6qfKv{tL=7ssib6}IL@)(C7j2Zq*R2wm|9-SK+7l5*nnaCb`D$$nvRY+WJZZ2a^0Pl_5k#L3Tjt$}4o z27!vyeehG=wm8erT8inkcIVaM3y_4oY~%{D(yNhdJ8<~`Ta2MEODD(0vT zMM54*st&?AaVtdsc`hpHO`Z037Zi!IA1QkZJQGXfd7+N!rm@itKhGuDdK#MW{^dA! zYO#fRh=KIszB#3y1aBlK-RI(>q>zsGyR%VLPF67wI|0Zh=A=(!OY+yWC1x5sU}hdp zLM)j$7T>eNAW)#MN5Dj0y0-ELVqvyXiX;>)VsRX=)*BE}-kkJ#EJO`$p;%$C>nalu zz9cdykitzY#Ly5uy$uinT5Y9+g{YzwJ6UN|BGgZr3JVl3e{Wdu`jNtDHUasxMezj& z4k&nrK={ya)JDF7Wf@rL@2sRS%Xu?wxL07~d@x1arfU@~>=%cKdc*V68l7Ko{&ZKn1%*^$q5a*5}T7~-9X@JTz zXsIw((#)k4j#NN2$$8S>01<^EK|%~_RJ29F1H`T?Ov=YB#6akg$$)4V>0=duu6A|jL=%8l>7h6^|eM=0h-n?9u+bk(< zTxmKtLml}+o$Oe%s<6x8N@$!b74Mdh?Yj1h{AHa2Y(tU_$NuA~=$JWm^*Hj!&}ZHI zHb#V}P5-*$O_8_e)Vc1lt%7934&D8eXL+1WEDvq*d3XX%%;Cbem1)W4m-rs`#p_=! z9)8fG>8Fq{f2r*JgIZX2P;6YW9v0b1MNFZMepKNt6hJxKMjKan;QbVbK}%j+XX^S(R;ggMT`V9a5R~ReZ}|PSkm9|*@W4mqiOvVnE?qD5Wqz2ldg&4`=i(?2w+l?jXr(ZTNpJ~Efn||S7bw(^gH3KD6b2xG!3fsjX1ON zRuq|Z+8vZ^xHY#e^+xeHHuKltH*5`l`u^3*rm*o?YM3+8%(XDf{0$FYRG<6ipLDxB zwEnWS<<~5#Aa;dV7g73YZW%+mlZr#@*Cr-DyS}yFwV~tl))43*QXJO+{UjX|^Q5Dq zar@JEZtQyd6lLWDN+FjYu3uvsyRY=Mpmih`;?R{qdF$q*P#$~L@njiZWkKq! zL0bv`@xAFRTW-n*+;MI_UX`|W~_nxt6uM^yMh8i2aNkgt?FX32FYg~ zlgU=SaCGHI~b7&20z_ZZ?< z+`Kwo895!&t_{*ITpV`@eqz9*-_+1EK>&AA3Vuq2ib8}1Q%sTf0X_$g5+<1bnutHoP6kvch&rvhkhTNd@Rwt z9LmpOlB93USY`Izu+QDovKb07tyoqi4`?=&rP$lSg1a&%X)3LLl?Ua&m}Hv zYYi<$i^{aStY1~r(#3cj>9THahBEHVp)-YU`=7({+z-z)-1fIl-C#6-%pI3CdQNy9 zEiDDQzr!G^HbPp9<1Bl$l+8!28g}@*L~|5+oy>5z{YAz~vuWJ{QHlzgegbhfRY@I$ zhTWShdm5c9N-0A2l6_p(Gqtod5N+XnxBZ$^mnDRj+f&UyhSiV6?BWzjyO*(wi>nFF z@=w(*V~imuad72x1KS|x7NWf@%jw??k!0IDrHP{v54^MX)KoQRQNh=1udJ~(xj0VtK&WOa5JJZ?Pjgjqku zjl$7g?;SobIoB#}k`hPnd(xWY1D|erj#FW;@)p{>gDnXOMQu;)ZH?zq%S%A6O#j0V z&=TrD1#JeO<(>Ex&)tG8gdJyGQU1Zk1zF-}2iRMXNixXu-1Ci<8Zbp3p7miM%wZp_ zMYz}y2w=SnofK91!P*b{=*LH}W*fwYH6KeNyu)MbSF$*M{Homw9OBvSsrj zm`Jvp?|bBfj*)+h5_=f3<@I~#g#=a6*+Ju94{Z%63P0n}+)HP}W{+L}V{^83@tW^U z6f*@CGU|i+U^TY(ZxDC^AX42V40gp}BGj1*EM>(2+mx{-kjE#5dQa%N62w%1fyt;R zMW~ZJTtQg&N^pKq^jNX21Ff=RTTYQewr?h+LcMlA6d-paN*am;(}?rTifuPrsURZR zvz&*EO3e~Luwpx*oFQ4UE$@Y8QGjTM(}!?cyd9OxIp1`)3HQZ}1UQRw)h)iyFfN^U&E$<8taf1?3j5u|OhE?C0-GR%h?*+K(AaPa$P`m^JKtuzARo_=p<5=~rj{x<1 zV1T9IWLUJLEAI&&v0uh@M6!hD^#D{n1{Q%|(;=`9z)}HqERcYgam%N90q35GpZbj< z18^NA9}SBvSd^pMcp*0q(0No>_=tdyt8ku#>OBk23(w@D3$Y2xwm~XQLkYNn zmCr%Ag;4iVVK6uXhMEIkk3pVwxXzK|5<1f#5a$p-D;OL`2_1l2lrlLWQUO82IV&~< zUV!id#NR-8w*ldYeWOAV(W4*u>pB390=x5wdPFk@P!{xLld-{gXph*PrzwCXb{^6D z-gMJhar@OMG6h;BY8NZ?56eFMk+I5c_r->(L-sRgE7dLREjdwq>G9K!@U%PXH5u;i g{|5hcB3E|wSXQ6no*uhp85{1Di8Ce~9WUSbpL%R*N&o-= literal 0 HcmV?d00001 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;