From 6c8108a20185395758e368240274eacb494ed7af Mon Sep 17 00:00:00 2001 From: Trevor Hutto Date: Fri, 26 Apr 2024 16:18:40 -0400 Subject: [PATCH] feat: add initial terraform (#1) --- .circleci/config.yml | 92 +++++++++++++ .github/CONTRIBUTING.md | 34 +++++ .github/pull_request_template.md | 20 +++ .gitignore | 32 +++++ .releaserc | 8 ++ .terraform-docs.yaml | 26 ++++ README.md | 227 +++++++++++++++++++++++++++++++ assets/fs-logo.png | Bin 0 -> 4202 bytes config.tf | 15 ++ examples/basic/main.tf | 39 ++++++ examples/reader_role/main.tf | 120 ++++++++++++++++ hooks/commit-msg | 5 + hooks/commit-msg.config.json | 21 +++ hooks/commit-msg.py | 63 +++++++++ init-repo.sh | 4 + main.tf | 107 +++++++++++++++ outputs.tf | 20 +++ variables.tf | 50 +++++++ 18 files changed, 883 insertions(+) create mode 100644 .circleci/config.yml create mode 100644 .github/CONTRIBUTING.md create mode 100644 .github/pull_request_template.md create mode 100644 .gitignore create mode 100644 .releaserc create mode 100644 .terraform-docs.yaml create mode 100644 assets/fs-logo.png create mode 100644 config.tf create mode 100644 examples/basic/main.tf create mode 100644 examples/reader_role/main.tf create mode 100755 hooks/commit-msg create mode 100644 hooks/commit-msg.config.json create mode 100755 hooks/commit-msg.py create mode 100755 init-repo.sh create mode 100644 main.tf create mode 100644 outputs.tf create mode 100644 variables.tf diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..c78e526 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,92 @@ +version: 2.1 + +executors: + terraform: + docker: + - image: cimg/deploy:2023.05 + node: + docker: + - image: cimg/node:current + + python: + docker: + - image: cimg/python:3.11.3 + + tf_docs: + docker: + - image: quay.io/terraform-docs/terraform-docs:0.17.0 + + +jobs: + terraform_check: + executor: terraform + steps: + - checkout + - run: + step_name: Run Terraform Validate + command: | + terraform init -backend=false + terraform validate + + terraform_docs: + executor: tf_docs + steps: + - checkout + - run: + step_name: Verify Terraform Documentation Generation + command: | + cp README.md /tmp + terraform-docs markdown . + diff /tmp/README.md README.md + + + msg_check: + executor: python + steps: + - checkout + - run: + command: | + if [ -z "${CIRCLE_PR_NUMBER}" ]; then + MSG="`git log -n 1 --pretty=%s`" + else + MSG="`curl -s https://api.github.com/repos/${CIRCLE_PR_REPONAME}/pulls/${CIRCLE_PR_NUMBER}|jq .title`" + MSG="${${MSG%%\"}##\"}" + if [ -z "$MSG" ]; then + MSG="`git log -n 1 --pretty=%s`" + fi + fi + + hooks/commit-msg.py "$MSG" + + version_bump: + executor: node + steps: + - checkout + - run: + step_name: Semantic Release + command: | + eval $(ssh-agent -s) + echo $DEPLOY_KEY | base64 -d > /tmp/deploy_key + chmod 600 /tmp/deploy_key + ssh-add /tmp/deploy_key > + jobs: + - terraform_check + - msg_check + - terraform_docs + release: + when: + and: + - equal: [ main, << pipeline.git.branch >> ] + jobs: + - version_bump diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..8d27d0a --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,34 @@ +# Welcome to Fullstory's Terraform contributing guide + +Thanks for your time in contributing to this project! Please read all the information below to properly +contribute with our workflow. + +## Issues + +- Make sure you test against the latest tagged version with the expected terraform version +- Re-run the `init-repo.sh` to ensure your local is the expected setup +- Provide a reprducible (or show) case. If you cannot accurately show the issue, it'll be difficult to fix + +## Setting up your workspace for dev + +- Run the `init-repo.sh` to ensure your dev workspace is correct with all tooling + +## Generating the README + +You can generate the README with HCL examples using `terraform-docs`. You can install `terraform-docs` by following [this guide](https://terraform-docs.io/user-guide/installation/). + +``` +terraform-docs markdown . +``` + +## Commit Messages + +This repo follows the [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/#summary) message style. This is strictly enforced by git hooks (which should have been activated by the `init-repo.sh`) and by CI. A small example is below: + +``` +feat: allow customization of cloudfront headers that are forwarded to origin +``` + +## Opening a PR + +Thanks for contributing! When you're ready to open a PR, you will need to fork this repo, push changes to your fork, and then open a PR here. Note: See [Working with forks](https://help.github.com/articles/working-with-forks/) for a better way to use git push. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..c097fbf --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,20 @@ +## Description + + + + +## Issue or Ticket + + + + + + + +## Checklist before submitting PR for review + +- [ ] This change requires a doc update, and I've included it +- [ ] My code follows the style guidelines of this project +- [ ] I have ensured my code is commented and any new terraform variables have proper descriptions diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..83bb124 --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log + +# Ignore any .tfvars files that are generated automatically for each Terraform run. Most +# .tfvars files are managed as part of configuration and so should be included in +# version control. +# +# example.tfvars + +# Ignore override files as they are usually used to override resources locally and so +# are not checked in +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Include override files you do wish to add to version control using negated pattern +# +# !example_override.tf + +# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan +# example: *tfplan* + +# VSCode +.vscode diff --git a/.releaserc b/.releaserc new file mode 100644 index 0000000..4af7763 --- /dev/null +++ b/.releaserc @@ -0,0 +1,8 @@ +branches: ["main"] +tagFormat: ${version} +plugins: + [ + "@semantic-release/commit-analyzer", + "@semantic-release/release-notes-generator", + "@semantic-release/github" + ] diff --git a/.terraform-docs.yaml b/.terraform-docs.yaml new file mode 100644 index 0000000..41bc714 --- /dev/null +++ b/.terraform-docs.yaml @@ -0,0 +1,26 @@ +formatter: "markdown" +output: + file: README.md + mode: inject + template: |- + + {{ .Content }} + +content: |- + {{ .Requirements }} + + {{ .Inputs }} + + {{ .Outputs }} + + ## Usage + + ```hcl + {{ include "examples/basic/main.tf" }} + ``` + + ### Creating a READER role + This module **does not** create a READER role. You can use the following example to create a READER role that will allow a user to use and read all objects _and_ all future objects in the database. + ```hcl + {{ include "examples/reader_role/main.tf" }} + ``` diff --git a/README.md b/README.md index d01ad03..f61b6b4 100644 --- a/README.md +++ b/README.md @@ -1 +1,228 @@ + + # terraform-snowflake-fullstory-warehouse-setup + +[![GitHub release](https://img.shields.io/github/release/fullstorydev/terraform-snowflake-fullstory-warehouse-setup.svg)](https://github.com/fullstorydev/terraform-snowflake-fullstory-warehouse-setup/releases/) + +This module creates all the proper roles, users, grants, and storage integrations so that Fullstory can connect to the database and load data. For more information checkout [this KB article](https://help.fullstory.com/hc/en-us/articles/6295349250199-Snowflake). + +This module **does not** create a reader role that can be used to view the data. To query the data inside Snowflake, you should create a role capable of reading the proper tables and columns according to your policies. + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 0.13 | +| [snowflake](#requirement\_snowflake) | >= 0.83.1 | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [database\_name](#input\_database\_name) | The name of the Snowflake database to use | `string` | n/a | yes | +| [fullstory\_cidr\_ipv4](#input\_fullstory\_cidr\_ipv4) | The CIDR block that Fullstory will use to connect to the Redshift cluster. | `string` | `""` | no | +| [fullstory\_data\_center](#input\_fullstory\_data\_center) | The data center where your Fullstory account is hosted. Either 'NA1' or 'EU1'. See https://help.fullstory.com/hc/en-us/articles/8901113940375-Fullstory-Data-Residency for more information. | `string` | `"NA1"` | no | +| [fullstory\_storage\_allowed\_locations](#input\_fullstory\_storage\_allowed\_locations) | The list of allowed locations for the storage provider. This is an advanced option and should only be changed if instructed by Fullstory. Ex. ://// | `list(string)` |
[
"gcs://fullstoryapp-warehouse-sync-bundles"
]
| no | +| [fullstory\_storage\_provider](#input\_fullstory\_storage\_provider) | The storage provider to use. Either 'S3', 'GCS' or 'AZURE'. This is an advanced option and should only be changed if instructed by Fullstory. | `string` | `"GCS"` | no | +| [suffix](#input\_suffix) | The suffix to append to the names of the resources created by this module so that the module can be instantiated many times. Must only contain letters. | `string` | n/a | yes | +| [warehouse\_name](#input\_warehouse\_name) | The name of the Snowflake warehouse to use. | `string` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| [gcs\_storage\_integration](#output\_gcs\_storage\_integration) | The name of the GCS storage integration that can be used in the Fullstory app when configuring the Snowflake integration. | +| [password](#output\_password) | The Fullstory password that can be used in the Fullstory app when configuring the Snowflake integration. | +| [role](#output\_role) | The Fullstory role that can be used in the Fullstory app when configuring the Snowflake integration. | +| [username](#output\_username) | The Fullstory username that can be used in the Fullstory app when configuring the Snowflake integration. | + +## Usage + +```hcl +resource "snowflake_database" "main" { + name = "MY_DATABASE" +} + +resource "snowflake_warehouse" "main" { + name = "MY_WAREHOUSE" + warehouse_size = "small" + auto_suspend = 60 +} + +module "fullstory_warehouse_setup" { + source = "fullstorydev/fullstory-warehouse-setup/snowflake" + providers = { + snowflake.account_admin = snowflake.account_admin + snowflake.security_admin = snowflake.security_admin + snowflake.sys_admin = snowflake.sys_admin + } + + database_name = snowflake_database.main.name + warehouse_name = snowflake_warehouse.main.name + fullstory_data_center = "NA1" + suffix = "ACME" # This should represent this module's unique identifier +} + +output "fullstory_warehouse_setup_role" { + value = module.fullstory_warehouse_setup.role +} + +output "fullstory_warehouse_setup_username" { + value = module.fullstory_warehouse_setup.username +} + +output "fullstory_warehouse_setup_password" { + value = module.fullstory_warehouse_setup.password +} + +output "fullstory_warehouse_setup_gcs_storage_integration" { + value = module.fullstory_warehouse_setup.gcs_storage_integration +} +``` + +### Creating a READER role +This module **does not** create a READER role. You can use the following example to create a READER role that will allow a user to use and read all objects _and_ all future objects in the database. +```hcl +resource "snowflake_role" "data_user_role" { + provider = snowflake.security_admin + name = "READER" +} + +resource "snowflake_grant_privileges_to_role" "data_user_database" { + provider = snowflake.security_admin + role_name = snowflake_role.data_user_role.name + + privileges = ["USAGE", "MONITOR"] + on_account_object { + object_name = "MY_DATABASE" + object_type = "DATABASE" + } +} + +resource "snowflake_grant_privileges_to_role" "data_user_schema" { + provider = snowflake.security_admin + role_name = snowflake_role.data_user_role.name + + privileges = [ + "USAGE", + "MONITOR", + ] + on_schema { + all_schemas_in_database = "MY_DATABASE" + } +} + +resource "snowflake_grant_privileges_to_role" "data_user_future_schema" { + provider = snowflake.security_admin + role_name = snowflake_role.data_user_role.name + + privileges = [ + "USAGE", + "MONITOR", + ] + on_schema { + future_schemas_in_database = "MY_DATABASE" + } +} + +resource "snowflake_grant_privileges_to_role" "data_user_tables" { + provider = snowflake.security_admin + role_name = snowflake_role.data_user_role.name + + privileges = ["SELECT"] + on_schema_object { + all { + object_type_plural = "TABLES" + in_database = "MY_DATABASE" + } + } +} + +resource "snowflake_grant_privileges_to_role" "data_user_future_tables" { + provider = snowflake.security_admin + role_name = snowflake_role.data_user_role.name + + privileges = ["SELECT"] + on_schema_object { + future { + object_type_plural = "TABLES" + in_database = "MY_DATABASE" + } + } +} + +resource "snowflake_grant_privileges_to_role" "data_user_views" { + provider = snowflake.security_admin + role_name = snowflake_role.data_user_role.name + + privileges = ["SELECT"] + on_schema_object { + all { + object_type_plural = "VIEWS" + in_database = snowflake_database.db.name + } + } +} + +resource "snowflake_grant_privileges_to_role" "data_user_future_views" { + provider = snowflake.security_admin + role_name = snowflake_role.data_user_role.name + + privileges = ["SELECT"] + on_schema_object { + future { + object_type_plural = "VIEWS" + in_database = snowflake_database.db.name + } + } +} + +resource "snowflake_grant_privileges_to_role" "data_user_mat_views" { + provider = snowflake.security_admin + role_name = snowflake_role.data_user_role.name + + privileges = ["SELECT"] + on_schema_object { + all { + object_type_plural = "MATERIALIZED VIEWS" + in_database = snowflake_database.db.name + } + } +} + + +resource "snowflake_grant_privileges_to_role" "data_user_future_mat_views" { + provider = snowflake.security_admin + role_name = snowflake_role.data_user_role.name + + privileges = ["SELECT"] + on_schema_object { + future { + object_type_plural = "MATERIALIZED VIEWS" + in_database = snowflake_database.db.name + } + } +} +``` + + +## Obtaining the output + +This module outputs the role, username, password and storage integration that can be pasted into Fullstory in order for Fullstory to connect to your database. After using this module, you must output the value of these variables in your root module ([see above example](#usage)). Once that is done, you should be able to access outputs with + +```bash +terraform output | pbcopy +``` + +The `password` output is a sensitive value. You need to use a slighly different command in order to see it. + +```bash +terraform output -raw | pbcopy +``` + +Alternatively, you can find all of the inputs in your Snowflake account. + +## Contributing + +See [CONTRIBUTING.md](https://github.com/fullstorydev/terraform-snowflake-fullstory-warehouse-setup/blob/main/.github/CONTRIBUTING.md) for best practices and instructions on setting up your dev environment. diff --git a/assets/fs-logo.png b/assets/fs-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..85c2ab38d46acda92bbcc50ba5ac910dc5aad5ef GIT binary patch literal 4202 zcmV-w5S8zVP)HpGufpzu*QF4H-(*tbm9H99)K&|uuP0|C@@*ZHL?g1Kh570P!fVKL2 zo!0#ynVnY{xHOFc!4HXwjrV!@SQbDE1cAZKJ2PNH8-&sBZtD8}Y&M(IH3r}JXh8Ik zKkg(%V|*ot#s2%*Y)UuW4Za;b+LVLx*GpX1U^X{xhM78;6Y&G&c#* z-njE$^W@v1h=^;B0Y&tJC}bjT5>}$o@M=9~6?(+H!i;(Vv;~%+d*?*cP2_iO@9yqJ zA+tVIa7>XmS_3-OXfVNf61@?wuhlZ_ocfn;la`#5F-D8V#=uZ%aaE31}6X%X(E9x&>QGc|L=i`>V}ER6d%BI4Si6G8KV=w)vB z1Q*6@)Ff2yBia1;jrnyCOpLg>=tR(fwdV`%Yvt?EYChZDg^)>=VwfaH6cG{83p#Ue zpR!rPrb|YSh)_oQDHF24&Sn!@0l)*XMzyDvN0a2^LctxK4#m`57>yaO=-T{MZlaY} z=6#g*?(kxJmrNSTC0Dv=`^V=*@2+Endk8v>qv64@50{~oe(loUb3_jyO_cNPrlc(uY1p%nagH}P<9N#P#2{d4|*_91XwI_~8H z#c@$=6Mv5}cbD6?!+%G79AS~S*Gf#AU3klV`X$wX{N^qG&7YMjCw$HOoeCOs9Czd^ za^J{k%Wx=9sY8fYOC^9YSg1$jW)Oy~rlUmRn9gNIlz$&44z2Or{K=?3y1rg56ejF} z%X6tbg%sV6A47 zJ01>z4(^&5mf%1qFc2cay19@EvaN+j1Ynzw!%vOCSvk;NK5lVa4->P1)fF;DAl_@j z+~Ut|LO@D=I8oZP)N*((V7_2(X4>Tj9tuJMWZFNJvYh3?H~7%Gpb=@PO6g%hRBR~3 zot?VMpv^=Ac(Z{_@1e;EPpLT)Xo4(knh2_3SQBB(w0Dm>LT)0Ke{3OawgdX`LKyIe zJdw%os>fx2r$C@j7Up-yN|nqm^n;2ErPKhd4no>zO7Xahv;awOP=ywE{sU@$-G~)K zxRyO?CuA%zLB3sOa3~9y1VUy-IcnP?Z0d}?BsXMvsGvre$qw^wFpD$I>X3Rt?HHP4 ztQ5m?yIAN|L4$`Txk86c9$@14^i0}L=q_NrIMh$XPfe(foI&c{7o zF>mpa@NtK~-uJ|7K|9h&&Q|!$2*racAtRF)9OXXRj)TmC_sY|53kaL`wDP*{ zY{@o1Bzb6(Ix+3SLk2Q>%aDpoh#TIe(VZoV7WUy!BKTTBRPG%m9RsTUvs0rL7Xi`} z{qPfyogWkTx#-uYiCIneFI`l;LymJYZBfKnM_WG_A)so{5HOgy3*AdKyKNRPc=!$y zT{!GwEY@&=kF@qAe`+ni6pEH@`(Qr5Qtru`PcO;$!(ujR8ckBNXttD#q*R&=Aeta> zkf0dKeQ-mb@&gzrjt&ubccX0_h8`hQr@tH%&m;zS&x&Kc1<9FoFx*kDOJ~fj)vtRN#=<+>RzvssNnvvdCoaPfUC%l;4 z|FvSw@o!+fP024Pp_P42&Aoj~q4~aI9HAe19@{<|JF38ZN`3Q4Orc$+_<{s3)xQjn z`zs}r4Sgb6$X=kK9u;lWBx0C<7dNN6vqORcF&KI9y#P~BibP(8hb-a}RMXjKR-c>W z(3&Qt<9?TMS_QIG;}qdyz8|`KmV&8*BNB+37Nc+KYUH9#Kr6%jxi}eUH3(SDC=4(YAD!0Ryk5+w z`@-zIGApFf+&224PdS>Kq*iML-M+Tf~ zD=(C!&eE=bDOEs-wCAkeZpS#B)c}K>2i|eT_}b=UiiHf%kH^U45OcjtDF5@Cr>0~~_BYnNqSe7i zO1BC*5c6?P4Sw5#$Nqn)C2}J)|E&~{$^tz%fr46}tAZ~m1;LtA3!~1?C=3_;js2o3 z4=UF?cdC4UDW9pp!@xZn#KY8O>m$0usUdbJN%L(ymgW^d^Y3q9L^m{|Ikkj%@ie|j zGoS7{zH^em7PWGnMBpH(tQLPG>h8m;JgB5*lA0x++VC)1%tpj|0Aa&C!9*6ZBM(99 z!~RzN5dKyehM6`h|h0gahORBW|4JK<{{)Q*_EDuvV z=T#LT#2WB0S>$0h3*rHR35<7fLE!9@87INd2g!+Bo|J|rZQ{u>*H(Hy{z@$%nuL$s zp3|BjDG8d8fRI_Xv%+ke`Q}rr>}^ce2K>!Q)sN=p0l%uKhMRiXfQPvkc^Cs;p^{_+ zR#gk@a>YUh&6&puwq(Hg3dt2(rgwCy)rfaRTNGPyL*gRVwpTb?=NLUT?)Hq_q%h2< zWni9o;asK^NZ{|tgVx(bl=z!0m(l|zrbC0XGSZyt6EJWa7 z^$F&0G)S~@>*&vG%ELI-)1>AE!(i9)&^8gG?HtQs*Lc3=)(O-tv`G(z=SAIsLP^rR zmFt<>P#i)XE}fx*6t)7~6}$*xTxET9|L$#xQk;Hfvc0v`10OD8VuS-4Q+;Fz;Jc0{ z68FdNiMt4KLr;DU6sqTDqI{ds`f=MNIlka~`J`ON1wZ)S*?ww5_#tSZO{CNkzVRu) zm64u&h;)g6vGDjZWULkT*M*QtU3Da1FO-Vt45dWN@}dP3`?02hEdn&K&NP}b8t^wu z6k}q<)X756g$j2Aco4|!6p%IlkK3=CPu+Z+1 zcl*HuZ`yzenX&B9r)E7TJ-)F+?$yb)c+9+@9T=7x%_{Rr5IeJFRI)>=SbF|w&M*Y|XJUzQ?niEG@cl@zoZXjz(o*v2cDr5+NE@mdwTX}I091sd4c^Xz)l zW-Lz4HXIo#iIxt41pa40J=&6;^iF7n$iu2-t|T>iv%)@qeC<0qF+0nzbn@h8wV6QhLHoxGv(Z|HFRezv#}}HA%$3(3 z+c!E4$qms;PNZKizUJS5ByRU*hV;O^F(D-#V;u5Y6{-WfuMPDKc{cF!9aPoLYrJN9 zWu{Xdz~otY_V|U(-ls~rfe2akQZl>BL6fw*_K#@;$lt=gwLVXUO*6=G@Mj!vDl{?Y z>MFbc`*M0L#--!pe8z9GkIbApMsBVh#Ph5gAIwon?ov%NZxaL!T-o=kOcHQDlHl2^ z>9PqB!abvIFsB}HZp@rVT|H0H2vj1xifgGtY167Xbp)%)oHf|vdKCzGCCddCHQeyM z37TSjODOLl37&WIo+ig80UBV-et?P$lXOl!Kn66px7Na@Fj>{>NM6;OHkz@x*pW;3 z7YbYTtMHoJ{kKgKG8TbCxz;wkYZo+a^NetFNPi#`ud`7PXq~{MYU0%ca+BG9zsb2> z0vX&3nQSeDrk$+ws)`Z;JE4|Bn%fQj=84VEqWZ8=>R&~_DI@tSWU|glkcU&hMz(K4FGP$r}d?ucl9bf&tuvcb{CjMia3gujp4OvGm5&AHKJT89}#LDLgpE)7iJQ$`UH8v+TlnDN*=>N3DH z3Ys;cn2LeW`6yr_dWPK-2*G#1@tFK&l~LD!0I3WUIk;WbiU0rr07*qoM6N<$g5q=X A5C8xG literal 0 HcmV?d00001 diff --git a/config.tf b/config.tf new file mode 100644 index 0000000..fb72b5f --- /dev/null +++ b/config.tf @@ -0,0 +1,15 @@ +terraform { + required_version = ">= 0.13" + + required_providers { + snowflake = { + source = "Snowflake-Labs/snowflake" + version = ">= 0.83.1" + configuration_aliases = [ + snowflake.account_admin, + snowflake.security_admin, + snowflake.sys_admin, + ] + } + } +} diff --git a/examples/basic/main.tf b/examples/basic/main.tf new file mode 100644 index 0000000..42ad0ae --- /dev/null +++ b/examples/basic/main.tf @@ -0,0 +1,39 @@ +resource "snowflake_database" "main" { + name = "MY_DATABASE" +} + +resource "snowflake_warehouse" "main" { + name = "MY_WAREHOUSE" + warehouse_size = "small" + auto_suspend = 60 +} + +module "fullstory_warehouse_setup" { + source = "fullstorydev/fullstory-warehouse-setup/snowflake" + providers = { + snowflake.account_admin = snowflake.account_admin + snowflake.security_admin = snowflake.security_admin + snowflake.sys_admin = snowflake.sys_admin + } + + database_name = snowflake_database.main.name + warehouse_name = snowflake_warehouse.main.name + fullstory_data_center = "NA1" + suffix = "ACME" # This should represent this module's unique identifier +} + +output "fullstory_warehouse_setup_role" { + value = module.fullstory_warehouse_setup.role +} + +output "fullstory_warehouse_setup_username" { + value = module.fullstory_warehouse_setup.username +} + +output "fullstory_warehouse_setup_password" { + value = module.fullstory_warehouse_setup.password +} + +output "fullstory_warehouse_setup_gcs_storage_integration" { + value = module.fullstory_warehouse_setup.gcs_storage_integration +} diff --git a/examples/reader_role/main.tf b/examples/reader_role/main.tf new file mode 100644 index 0000000..45aea8d --- /dev/null +++ b/examples/reader_role/main.tf @@ -0,0 +1,120 @@ +resource "snowflake_role" "data_user_role" { + provider = snowflake.security_admin + name = "READER" +} + +resource "snowflake_grant_privileges_to_role" "data_user_database" { + provider = snowflake.security_admin + role_name = snowflake_role.data_user_role.name + + privileges = ["USAGE", "MONITOR"] + on_account_object { + object_name = "MY_DATABASE" + object_type = "DATABASE" + } +} + +resource "snowflake_grant_privileges_to_role" "data_user_schema" { + provider = snowflake.security_admin + role_name = snowflake_role.data_user_role.name + + privileges = [ + "USAGE", + "MONITOR", + ] + on_schema { + all_schemas_in_database = "MY_DATABASE" + } +} + +resource "snowflake_grant_privileges_to_role" "data_user_future_schema" { + provider = snowflake.security_admin + role_name = snowflake_role.data_user_role.name + + privileges = [ + "USAGE", + "MONITOR", + ] + on_schema { + future_schemas_in_database = "MY_DATABASE" + } +} + +resource "snowflake_grant_privileges_to_role" "data_user_tables" { + provider = snowflake.security_admin + role_name = snowflake_role.data_user_role.name + + privileges = ["SELECT"] + on_schema_object { + all { + object_type_plural = "TABLES" + in_database = "MY_DATABASE" + } + } +} + +resource "snowflake_grant_privileges_to_role" "data_user_future_tables" { + provider = snowflake.security_admin + role_name = snowflake_role.data_user_role.name + + privileges = ["SELECT"] + on_schema_object { + future { + object_type_plural = "TABLES" + in_database = "MY_DATABASE" + } + } +} + +resource "snowflake_grant_privileges_to_role" "data_user_views" { + provider = snowflake.security_admin + role_name = snowflake_role.data_user_role.name + + privileges = ["SELECT"] + on_schema_object { + all { + object_type_plural = "VIEWS" + in_database = snowflake_database.db.name + } + } +} + +resource "snowflake_grant_privileges_to_role" "data_user_future_views" { + provider = snowflake.security_admin + role_name = snowflake_role.data_user_role.name + + privileges = ["SELECT"] + on_schema_object { + future { + object_type_plural = "VIEWS" + in_database = snowflake_database.db.name + } + } +} + +resource "snowflake_grant_privileges_to_role" "data_user_mat_views" { + provider = snowflake.security_admin + role_name = snowflake_role.data_user_role.name + + privileges = ["SELECT"] + on_schema_object { + all { + object_type_plural = "MATERIALIZED VIEWS" + in_database = snowflake_database.db.name + } + } +} + + +resource "snowflake_grant_privileges_to_role" "data_user_future_mat_views" { + provider = snowflake.security_admin + role_name = snowflake_role.data_user_role.name + + privileges = ["SELECT"] + on_schema_object { + future { + object_type_plural = "MATERIALIZED VIEWS" + in_database = snowflake_database.db.name + } + } +} diff --git a/hooks/commit-msg b/hooks/commit-msg new file mode 100755 index 0000000..9da530d --- /dev/null +++ b/hooks/commit-msg @@ -0,0 +1,5 @@ +#!/bin/sh +# Now process commit message +msg=$(head -1 $1) + +./hooks/commit-msg.py "$msg" diff --git a/hooks/commit-msg.config.json b/hooks/commit-msg.config.json new file mode 100644 index 0000000..51a544d --- /dev/null +++ b/hooks/commit-msg.config.json @@ -0,0 +1,21 @@ +{ + "enabled": true, + "revert": true, + "length": { + "min": 5, + "max": 52 + }, + "types": [ + "build", + "ci", + "docs", + "feat", + "fix", + "perf", + "refactor", + "style", + "test", + "chore" + ] +} + diff --git a/hooks/commit-msg.py b/hooks/commit-msg.py new file mode 100755 index 0000000..95b6f73 --- /dev/null +++ b/hooks/commit-msg.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 + +from json import load as jload +from os.path import exists +import re +from sys import exit, argv + + +CONFIG_FILE = "hooks/commit-msg.config.json" +CONFIG = dict() +class COLORS: + HEADER = '\033[95m' + OKBLUE = '\033[94m' + OKCYAN = '\033[96m' + OKGREEN = '\033[92m' + WARNING = '\033[93m' + FAIL = '\033[91m' + ENDC = '\033[0m' + BOLD = '\033[1m' + UNDERLINE = '\033[4m' + +if not exists(CONFIG_FILE): + print(f"{COLORS.WARNING}WARNING: No conventional commit config file found. Skipping checks{COLORS.ENDC}") + exit(0) +elif len(argv) != 2: + print(f"{COLORS.FAIL}ERROR: No commit message passed as argument. Aborting.{COLORS.ENDC}") + exit(1) + + +with open(CONFIG_FILE) as fh: + CONFIG = jload(fh) + +pattern = "^(" +if CONFIG['revert']: + CONFIG['types'].append('revert') + +for msgType in CONFIG['types']: + pattern = f"{pattern}{msgType}|" + +# If an opening bracket comes immediately after the type, +# It must be closed and contain a scope. +# The second thing is that the type and/or scope must be followed with a colon symbol (:). +pattern = f"{pattern})(\\(.+\\))?: " + +msg = argv[1] +passCheck = True + +if not re.match(pattern, msg): + passCheck = False + errMsg = f"commit message does not match required pattern!\nAllowed commit types: {','.join(CONFIG['types'])}" + +elif len(msg.split(':')[1]) > CONFIG['length']['max'] or len(msg.split(':')[1]) < CONFIG['length']['min']: + passCheck = False + errMsg = f"commit message is not between min [{CONFIG['length']['min']}] and max [{CONFIG['length']['max']}] characters!" + + +if not passCheck: + print(f"{COLORS.BOLD}{COLORS.FAIL}[INVALID COMMIT MESSAGE]{COLORS.ENDC}") + print("=========================================================") + print(f"{COLORS.FAIL}ERROR: {errMsg}{COLORS.ENDC}") + print(f"{COLORS.WARNING}Check https://www.conventionalcommits.org/en/v1.0.0/#summary for correct syntax{COLORS.ENDC}") + exit(1) + diff --git a/init-repo.sh b/init-repo.sh new file mode 100755 index 0000000..2c3043a --- /dev/null +++ b/init-repo.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +echo "Enabling commit check" +cp ./hooks/commit-msg ./.git/hooks/ diff --git a/main.tf b/main.tf new file mode 100644 index 0000000..a9902a0 --- /dev/null +++ b/main.tf @@ -0,0 +1,107 @@ +locals { + fullstory_cidr_ipv4 = var.fullstory_cidr_ipv4 != "" ? var.fullstory_cidr_ipv4 : (var.fullstory_data_center == "EU1" ? "34.89.210.80/29" : "8.35.195.0/29") + suffix = upper(var.suffix) +} + +provider "snowflake" { + alias = "account_admin" +} + +provider "snowflake" { + alias = "security_admin" +} + +provider "snowflake" { + alias = "sys_admin" +} + +resource "snowflake_role" "main" { + provider = snowflake.security_admin + name = "FULLSTORY_WAREHOUSE_SETUP_${local.suffix}" +} + +resource "snowflake_grant_privileges_to_role" "database" { + provider = snowflake.security_admin + all_privileges = true + role_name = snowflake_role.main.name + on_account_object { + object_type = "DATABASE" + object_name = var.database_name + } +} + +resource "snowflake_grant_privileges_to_role" "warehouse" { + provider = snowflake.security_admin + role_name = snowflake_role.main.name + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = var.warehouse_name + } +} + +resource "random_password" "main" { + length = 16 + special = true + override_special = "!#$%&*()-_=+[]{}<>:?" +} + +resource "snowflake_user" "main" { + provider = snowflake.security_admin + name = "FULLSTORY_WAREHOUSE_SETUP_${local.suffix}" + default_warehouse = var.warehouse_name + default_role = snowflake_role.main.name + password = random_password.main.result +} + +resource "snowflake_grant_privileges_to_role" "user" { + provider = snowflake.security_admin + role_name = snowflake_role.main.name + privileges = ["MONITOR"] + on_account_object { + object_type = "USER" + object_name = snowflake_user.main.name + } +} + +resource "snowflake_role_grants" "main" { + provider = snowflake.security_admin + role_name = snowflake_role.main.name + users = [ + snowflake_user.main.name, + ] +} + +resource "snowflake_storage_integration" "main" { + provider = snowflake.account_admin + name = "FULLSTORY_STAGE_${local.suffix}" + comment = "Stage for FullStory data" + type = "EXTERNAL_STAGE" + enabled = true + + storage_provider = var.fullstory_storage_provider + storage_allowed_locations = var.fullstory_storage_allowed_locations +} + +resource "snowflake_grant_privileges_to_role" "integration" { + provider = snowflake.security_admin + role_name = snowflake_role.main.name + privileges = ["USAGE"] + on_account_object { + object_type = "INTEGRATION" + object_name = snowflake_storage_integration.main.name + } +} + +resource "snowflake_network_policy" "main" { + provider = snowflake.security_admin + name = "FULLSTORY_NETWORK_POLICY_${local.suffix}" + allowed_ip_list = [local.fullstory_cidr_ipv4] +} + +resource "snowflake_network_policy_attachment" "main" { + provider = snowflake.security_admin + network_policy_name = snowflake_network_policy.main.name + set_for_account = false + users = [snowflake_user.main.name] +} diff --git a/outputs.tf b/outputs.tf new file mode 100644 index 0000000..4205d94 --- /dev/null +++ b/outputs.tf @@ -0,0 +1,20 @@ +output "role" { + description = "The Fullstory role that can be used in the Fullstory app when configuring the Snowflake integration." + value = snowflake_role.main.name +} + +output "username" { + description = "The Fullstory username that can be used in the Fullstory app when configuring the Snowflake integration." + value = snowflake_user.main.login_name +} + +output "password" { + description = "The Fullstory password that can be used in the Fullstory app when configuring the Snowflake integration." + value = snowflake_user.main.password + sensitive = true +} + +output "gcs_storage_integration" { + description = "The name of the GCS storage integration that can be used in the Fullstory app when configuring the Snowflake integration." + value = snowflake_storage_integration.main.name +} diff --git a/variables.tf b/variables.tf new file mode 100644 index 0000000..21704cd --- /dev/null +++ b/variables.tf @@ -0,0 +1,50 @@ +variable "database_name" { + type = string + description = "The name of the Snowflake database to use" +} + +variable "fullstory_cidr_ipv4" { + type = string + description = "The CIDR block that Fullstory will use to connect to the Redshift cluster." + default = "" +} + +variable "fullstory_data_center" { + type = string + description = "The data center where your Fullstory account is hosted. Either 'NA1' or 'EU1'. See https://help.fullstory.com/hc/en-us/articles/8901113940375-Fullstory-Data-Residency for more information." + default = "NA1" + validation { + condition = var.fullstory_data_center == "NA1" || var.fullstory_data_center == "EU1" + error_message = "The data center must be either 'NA1' or 'EU1'." + } +} + +variable "fullstory_storage_allowed_locations" { + type = list(string) + description = "The list of allowed locations for the storage provider. This is an advanced option and should only be changed if instructed by Fullstory. Ex. :////" + default = ["gcs://fullstoryapp-warehouse-sync-bundles"] +} + +variable "fullstory_storage_provider" { + type = string + description = "The storage provider to use. Either 'S3', 'GCS' or 'AZURE'. This is an advanced option and should only be changed if instructed by Fullstory." + validation { + condition = var.fullstory_storage_provider == "S3" || var.fullstory_storage_provider == "GCS" || var.fullstory_storage_provider == "AZURE" + error_message = "The storage provider must be either 'S3', 'GCS', or 'AZURE'." + } + default = "GCS" +} + +variable "suffix" { + type = string + description = "The suffix to append to the names of the resources created by this module so that the module can be instantiated many times. Must only contain letters." + validation { + condition = can(regex("^[a-zA-Z]+$", var.suffix)) + error_message = "The suffix must only contain letters." + } +} + +variable "warehouse_name" { + type = string + description = "The name of the Snowflake warehouse to use." +}