diff --git a/.github/workflows/action.yml b/.github/workflows/action.yml new file mode 100644 index 0000000..d21680f --- /dev/null +++ b/.github/workflows/action.yml @@ -0,0 +1,52 @@ +name: "Terraform-MongoDB-User Module" + +on: + pull_request: + branches: + - main + +jobs: + terraform-checks: + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v2 + + - uses: hashicorp/setup-terraform@v1 + with: + terraform_version: 1.6.3 + + - uses: terraform-linters/setup-tflint@v3 + name: Setup TFLint + with: + tflint_version: v0.44.1 + + - name: Terraform Init + id: tf-init + run: terraform init + + - name: Terraform Validate + id: tf-validate + run: terraform validate + + - name: Terraform Format Check + id: tf-fmt-check + run: terraform fmt -recursive -check + continue-on-error: true + + - name: Show version + run: tflint --version + + - name: Init TFLint + run: tflint --init + env: + # https://github.com/terraform-linters/tflint/blob/master/docs/user-guide/plugins.md#avoiding-rate-limiting + GITHUB_TOKEN: ${{ github.token }} + + - name: Run TFLint + run: tflint -f compact + + - name: TFSec + uses: aquasecurity/tfsec-action@v1.0.0 + with: + soft_fail: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7257406 --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +.DS_Store +*.lock.hcl + +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log +crash.*.log + +# Exclude all .tfvars files, which are likely to contain sensitive data, such as +# password, private keys, and other secrets. These should not be part of version +# control as they are data points which are potentially sensitive and subject +# to change depending on the environment. +# *.tfvars +*.tfvars.json + +# 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* + +# Ignore CLI configuration files +.terraformrc +terraform.rc diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..c0f0a24 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,15 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.3.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files +- repo: https://github.com/antonbabenko/pre-commit-terraform + rev: v1.64.1 + hooks: + - id: terraform_validate + - id: terraform_fmt + - id: terraform_tflint + - id: terraform_tfsec diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6f90ae2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) 2023, ridwanfs + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e2af7f5 --- /dev/null +++ b/README.md @@ -0,0 +1,209 @@ +# Terraform MongoDB User + +This is a Terraform module for managing user access at MongoDB. You can use this module both for commercial or non-commercial purposes. + +Currently, you can manage these resources in MongoDB by using this module: + +- users +- roles + +Tested in: + +- MongoDB + +## A. Prerequisites + +Requirements: + +- Terraform with version >= 1.4 +- Kaginari/mongodb +- Hashicorp/random + +Before we continue to use the module, please pay attention to these terms for `database `in this module: + +- `role_db`, selected database for storing role configuration. Default value from tf-provider is `admin` +- `target_db`, selected database which the privilege will be assigned to. +- `auth_database`, database that will be used by the user for authentication + +## B. How to use this module for your Terraform project ? + +- Copy `example/basic-1` project from this module. You can extend it as per your requirements +- Configure MongoDB host and port by modifying the `providers.tf`. For example `localhost` and `27017` + - If you want to use another authentication method, [please read more at the provider documentation](https://registry.terraform.io/providers/Kaginari/mongodb/latest/docs) +- Configure `TF_VAR_mongodb_username` and `TF_VAR_mongodb_password` as environment variables. For example: + +``` +$ export TF_VAR_mongodb_username=root +$ export TF_VAR_mongodb_password=example +``` + +- Check `terraform.tfvars` inside the Project. Please try to see how the variables configured. +- Adjust the tfvars based on your requirements. The tfvars is just example. Then, Save it +- Run these commands: + +``` +$ terraform init +$ terraform plan +``` +This is the output when you run terraform plan successfully: + +``` +... + + # module.tf_mongodb_user.random_password.password["septian"] will be created + + resource "random_password" "password" { + + bcrypt_hash = (sensitive value) + + id = (known after apply) + + length = 16 + + lower = true + + min_lower = 0 + + min_numeric = 0 + + min_special = 0 + + min_upper = 0 + + number = true + + numeric = true + + override_special = "!#$%&*()-_=+[]{}<>:?" + + result = (sensitive value) + + special = true + + upper = true + } + +Plan: 10 to add, 0 to change, 0 to destroy. + +Changes to Outputs: + + mongodb_roles = { + + developer = "admin" + + guest = "admin" + } + + mongodb_users = { + + bejo = "reporting" + + fadjar = "reporting" + + ridwan = "reporting" + + septian = "reporting" + } + +``` + +After you feel confidence with the terraform plan output, let's apply it. + +``` +$ terraform apply -auto-approve +``` + +- If it succeed, you must see this kind of output on your terminal + +``` +... + +module.tf_mongodb_user.mongodb_db_role.roles["developer"]: Creation complete after 0s [id=YWRtaW4uZGV2ZWxvcGVy] +module.tf_mongodb_user.mongodb_db_role.roles["guest"]: Creation complete after 0s [id=YWRtaW4uZ3Vlc3Q=] +module.tf_mongodb_user.random_password.password["septian"]: Creation complete after 0s [id=none] +module.tf_mongodb_user.random_password.password["bejo"]: Creation complete after 0s [id=none] +module.tf_mongodb_user.random_password.password["fadjar"]: Creation complete after 0s [id=none] +module.tf_mongodb_user.random_password.password["ridwan"]: Creation complete after 0s [id=none] +module.tf_mongodb_user.mongodb_db_user.users["bejo"]: Creating... +module.tf_mongodb_user.mongodb_db_user.users["fadjar"]: Creating... +module.tf_mongodb_user.mongodb_db_user.users["septian"]: Creating... +module.tf_mongodb_user.mongodb_db_user.users["ridwan"]: Creating... +module.tf_mongodb_user.mongodb_db_user.users["bejo"]: Creation complete after 0s [id=cmVwb3J0aW5nLmJlam8=] +module.tf_mongodb_user.mongodb_db_user.users["ridwan"]: Creation complete after 0s [id=cmVwb3J0aW5nLnJpZHdhbg==] +module.tf_mongodb_user.mongodb_db_user.users["septian"]: Creation complete after 0s [id=cmVwb3J0aW5nLnNlcHRpYW4=] +module.tf_mongodb_user.mongodb_db_user.users["fadjar"]: Creation complete after 0s [id=cmVwb3J0aW5nLmZhZGphcg==] + +Apply complete! Resources: 10 added, 0 changed, 0 destroyed. + +Outputs: + +mongodb_roles = { + "developer" = "admin" + "guest" = "admin" +} +mongodb_users = { + "bejo" = "reporting" + "fadjar" = "reporting" + "ridwan" = "reporting" + "septian" = "reporting" +} +``` + +You will see at your MongoDB that users and roles are created once the terraform applied. + +## C. Understanding tfvars scenarios + +There are some scenarios that you could choose when using this module. For example: + +1. user could be assined without any roles + +``` +mongodb_users = [ + { + name = "bejo" + auth_database = "reporting" + roles = [] + }, +] + +``` + +2. user could be assigned with default roles (e.g. readAnyDatabase) + +``` +mongodb_users = [ + { + name = "septian" + auth_database = "reporting" + roles = [ + { + name = "readAnyDatabase", + role_db = "admin" + } + ] + }, +] + +``` + +3. user could be assigned with custom roles (e.g. developer) + +``` +mongodb_users = [ + { + name = "ridwan" + auth_database = "reporting" + roles = [ + { + name = "developer", + role_db = "admin" + } + ] + }, +] +``` + +## D. Ensuring quality + +I am trying to follow these approaches for ensuring quality of the tf-module: + +- **validate**, ensure my Terraform module is in correct configuration based on Terraform guideline +- **auto-format**, ensure my Terraform script is edited with correct format based on Terraform guideline +- **linter**, ensure my Terraform script is in correct format based on Terraform guideline +- **security**, ensure my Terraform module is free from CVE and stay compliance +- **automation**, run all above steps by using automation tool to improve development time and keep best quality before or after merging to Git repository + + +The tools: + +- [terraform validate](https://developer.hashicorp.com/terraform/cli/commands) +- [terraform fmt](https://developer.hashicorp.com/terraform/cli/commands) +- [tflint](https://github.com/terraform-lint48ers/tflint) +- [tfsec](https://github.com/aquasecurity/tfsec) +- [Pre-commit](https://pre-commit.com/) +- Github Action [Setup Terraform pipeline](https://github.com/hashicorp/setup-terraform) + +## E. How to contribute ? + +If you find any issue, you can raise it here at our [Issue Tracker](https://github.com/ridwanbejo/terraform-mongodb-user/issues) + +If you have something that you want to merge to this repo, just raise [Pull Requests](https://github.com/ridwanbejo/terraform-mongodb-user/pulls) + +Ensure that you install all the tools from section D. for development purpose. diff --git a/examples/config-1-basic/locals.tf b/examples/config-1-basic/locals.tf new file mode 100644 index 0000000..d555d86 --- /dev/null +++ b/examples/config-1-basic/locals.tf @@ -0,0 +1,3 @@ +locals { + +} diff --git a/examples/config-1-basic/main.tf b/examples/config-1-basic/main.tf new file mode 100644 index 0000000..923d208 --- /dev/null +++ b/examples/config-1-basic/main.tf @@ -0,0 +1,7 @@ +module "tf_postgres_config" { + source = "../../modules/config" + extensions = var.postgres_extension + replication_slots = var.postgres_replication_slots + + physical_replication_slots = var.postgres_physical_replication_slots +} diff --git a/examples/config-1-basic/outputs.tf b/examples/config-1-basic/outputs.tf new file mode 100644 index 0000000..17d4aea --- /dev/null +++ b/examples/config-1-basic/outputs.tf @@ -0,0 +1,5 @@ +output "postgres_extension" { + description = "Current PostgreSQL Extensions" + value = module.tf_postgres_config.postgres_extensions +} + diff --git a/examples/config-1-basic/providers.tf b/examples/config-1-basic/providers.tf new file mode 100644 index 0000000..a0f1e10 --- /dev/null +++ b/examples/config-1-basic/providers.tf @@ -0,0 +1,24 @@ +terraform { + required_version = ">= 1.4" + + required_providers { + postgresql = { + source = "cyrilgdn/postgresql" + version = "1.21.1-beta.1" + } + + random = { + source = "hashicorp/random" + version = "3.5.1" + } + } +} + +provider "postgresql" { + host = "127.0.0.1" + port = "5432" + database = "postgres" + username = "postgres" + password = "example" + sslmode = "disable" +} diff --git a/examples/config-1-basic/terraform.tfvars b/examples/config-1-basic/terraform.tfvars new file mode 100644 index 0000000..bddd2ef --- /dev/null +++ b/examples/config-1-basic/terraform.tfvars @@ -0,0 +1,22 @@ +postgres_extension = [ + { + name = "postgres_fdw" + database = "dev_ms_auth" + }, + { + name = "postgres_fdw" + database = "dev_ms_payment" + }, +] + +postgres_physical_replication_slots = [ + { + name = "my_physical_slot_1" + }, + { + name = "my_physical_slot_2" + }, + { + name = "my_physical_slot_3" + } +] \ No newline at end of file diff --git a/examples/config-1-basic/variables.tf b/examples/config-1-basic/variables.tf new file mode 100644 index 0000000..7728c8e --- /dev/null +++ b/examples/config-1-basic/variables.tf @@ -0,0 +1,14 @@ +variable "postgres_extension" { + type = list(any) + default = [] +} + +variable "postgres_replication_slots" { + type = list(any) + default = [] +} + +variable "postgres_physical_replication_slots" { + type = list(any) + default = [] +} diff --git a/examples/database-1-basic/locals.tf b/examples/database-1-basic/locals.tf new file mode 100644 index 0000000..6c1feef --- /dev/null +++ b/examples/database-1-basic/locals.tf @@ -0,0 +1,3 @@ +locals { + databases = var.postgres_databases +} diff --git a/examples/database-1-basic/main.tf b/examples/database-1-basic/main.tf new file mode 100644 index 0000000..ca30336 --- /dev/null +++ b/examples/database-1-basic/main.tf @@ -0,0 +1,5 @@ +module "tf_postgres_database" { + source = "../../modules/database" + + databases = local.databases +} diff --git a/examples/database-1-basic/outputs.tf b/examples/database-1-basic/outputs.tf new file mode 100644 index 0000000..5be3fb8 --- /dev/null +++ b/examples/database-1-basic/outputs.tf @@ -0,0 +1,9 @@ +output "postgres_databases" { + description = "Current PostgreSQL databases" + value = module.tf_postgres_database.postgres_databases +} + +output "postgres_schemas" { + description = "Current PostgreSQL schemas" + value = module.tf_postgres_database.postgres_schemas +} diff --git a/examples/database-1-basic/providers.tf b/examples/database-1-basic/providers.tf new file mode 100644 index 0000000..a0f1e10 --- /dev/null +++ b/examples/database-1-basic/providers.tf @@ -0,0 +1,24 @@ +terraform { + required_version = ">= 1.4" + + required_providers { + postgresql = { + source = "cyrilgdn/postgresql" + version = "1.21.1-beta.1" + } + + random = { + source = "hashicorp/random" + version = "3.5.1" + } + } +} + +provider "postgresql" { + host = "127.0.0.1" + port = "5432" + database = "postgres" + username = "postgres" + password = "example" + sslmode = "disable" +} diff --git a/examples/database-1-basic/terraform.tfvars b/examples/database-1-basic/terraform.tfvars new file mode 100644 index 0000000..498aacd --- /dev/null +++ b/examples/database-1-basic/terraform.tfvars @@ -0,0 +1,25 @@ +postgres_databases = [ + { + name = "dev_ms_auth" + owner = "dev_ms_auth" + allow_connections = true + schemas = [ + { + name = "auth" + owner = "dev_ms_auth" + policies = [] + }, + { + name = "user" + owner = "dev_ms_auth" + policies = [] + }, + ] + }, + { + name = "dev_ms_payment" + owner = "dev_ms_payment" + allow_connections = true + schemas = [] + } +] \ No newline at end of file diff --git a/examples/database-1-basic/variables.tf b/examples/database-1-basic/variables.tf new file mode 100644 index 0000000..3188699 --- /dev/null +++ b/examples/database-1-basic/variables.tf @@ -0,0 +1,12 @@ +variable "postgres_databases" { + type = list(any) + default = [] +} + +# variable "postgres_username" { +# type = string +# } + +# variable "postgres_password" { +# type = string +# } diff --git a/examples/fdw-1-basic/main.tf b/examples/fdw-1-basic/main.tf new file mode 100644 index 0000000..a101277 --- /dev/null +++ b/examples/fdw-1-basic/main.tf @@ -0,0 +1,5 @@ +module "tf_postgres_fdw" { + source = "../../modules/foreign_data_wrapper" + fdw_servers = var.pg_fdw_servers + fdw_user_mappings = var.pg_user_mappings +} diff --git a/examples/fdw-1-basic/outputs.tf b/examples/fdw-1-basic/outputs.tf new file mode 100644 index 0000000..1704025 --- /dev/null +++ b/examples/fdw-1-basic/outputs.tf @@ -0,0 +1,9 @@ +output "pg_fdw_servers" { + description = "Current PostgreSQL FDW servers" + value = module.tf_postgres_fdw.postgres_fdw_servers +} + +output "pg_user_mappings" { + description = "Current PostgreSQL FDW user mappings" + value = module.tf_postgres_fdw.postgres_fdw_user_mappings +} \ No newline at end of file diff --git a/examples/fdw-1-basic/providers.tf b/examples/fdw-1-basic/providers.tf new file mode 100644 index 0000000..ca1546e --- /dev/null +++ b/examples/fdw-1-basic/providers.tf @@ -0,0 +1,24 @@ +terraform { + required_version = ">= 1.4" + + required_providers { + postgresql = { + source = "cyrilgdn/postgresql" + version = "1.21.0" + } + + random = { + source = "hashicorp/random" + version = "3.5.1" + } + } +} + +provider "postgresql" { + host = "127.0.0.1" + port = "5432" + database = "postgres" + username = "postgres" + password = "example" + sslmode = "disable" +} diff --git a/examples/fdw-1-basic/terrraform.tfvars b/examples/fdw-1-basic/terrraform.tfvars new file mode 100644 index 0000000..6f84203 --- /dev/null +++ b/examples/fdw-1-basic/terrraform.tfvars @@ -0,0 +1,11 @@ +pg_fdw_servers = [ + { + server_name = "dev_ms_auth_fdw" + fdw_name = "postgres_fdw" + options = { + host = "localhost" + dbname = "dev_ms_auth" + port = "5432" + } + } + ] \ No newline at end of file diff --git a/examples/fdw-1-basic/variables.tf b/examples/fdw-1-basic/variables.tf new file mode 100644 index 0000000..1df3e3d --- /dev/null +++ b/examples/fdw-1-basic/variables.tf @@ -0,0 +1,9 @@ +variable "pg_fdw_servers" { + type = list(any) + default = [] +} + +variable "pg_user_mappings" { + type = list(any) + default = [] +} diff --git a/examples/grant-1-basic/locals.tf b/examples/grant-1-basic/locals.tf new file mode 100644 index 0000000..d555d86 --- /dev/null +++ b/examples/grant-1-basic/locals.tf @@ -0,0 +1,3 @@ +locals { + +} diff --git a/examples/grant-1-basic/main.tf b/examples/grant-1-basic/main.tf new file mode 100644 index 0000000..678db24 --- /dev/null +++ b/examples/grant-1-basic/main.tf @@ -0,0 +1,5 @@ +module "tf_postgres_grant" { + source = "../../modules/grant" + grants = var.pg_grants + grant_roles = var.pg_grant_roles +} diff --git a/examples/grant-1-basic/outputs.tf b/examples/grant-1-basic/outputs.tf new file mode 100644 index 0000000..93fef49 --- /dev/null +++ b/examples/grant-1-basic/outputs.tf @@ -0,0 +1,9 @@ +output "pg_grants" { + description = "Current PostgreSQL grants" + value = module.tf_postgres_grant.postgres_grants +} + +output "pg_grant_roles" { + description = "Current PostgreSQL grant roles" + value = module.tf_postgres_grant.postgres_grant_roles +} diff --git a/examples/grant-1-basic/providers.tf b/examples/grant-1-basic/providers.tf new file mode 100644 index 0000000..ca1546e --- /dev/null +++ b/examples/grant-1-basic/providers.tf @@ -0,0 +1,24 @@ +terraform { + required_version = ">= 1.4" + + required_providers { + postgresql = { + source = "cyrilgdn/postgresql" + version = "1.21.0" + } + + random = { + source = "hashicorp/random" + version = "3.5.1" + } + } +} + +provider "postgresql" { + host = "127.0.0.1" + port = "5432" + database = "postgres" + username = "postgres" + password = "example" + sslmode = "disable" +} diff --git a/examples/grant-1-basic/terraform.tfvars b/examples/grant-1-basic/terraform.tfvars new file mode 100644 index 0000000..a017af0 --- /dev/null +++ b/examples/grant-1-basic/terraform.tfvars @@ -0,0 +1,16 @@ +pg_grants = [ + { + database = "dev_ms_auth" + role = "dev_ms_auth_readonly" + schema = "public" + object_type = "table" + privileges = ["SELECT"] + }, + { + database = "dev_ms_auth" + role = "dev_ms_auth" + schema = "public" + object_type = "table" + privileges = ["SELECT", "INSERT", "UPDATE", "DELETE"] + } +] diff --git a/examples/grant-1-basic/variables.tf b/examples/grant-1-basic/variables.tf new file mode 100644 index 0000000..fb71063 --- /dev/null +++ b/examples/grant-1-basic/variables.tf @@ -0,0 +1,9 @@ +variable "pg_grants" { + type = list(any) + default = [] +} + +variable "pg_grant_roles" { + type = list(any) + default = [] +} diff --git a/examples/pubsub-1-basic/main.tf b/examples/pubsub-1-basic/main.tf new file mode 100644 index 0000000..c0e511e --- /dev/null +++ b/examples/pubsub-1-basic/main.tf @@ -0,0 +1,5 @@ +module "tf_postgres_pubsub" { + source = "../../modules/pubsub" + publications = var.pg_publications + subscriptions = var.pg_subscriptions +} diff --git a/examples/pubsub-1-basic/outputs.tf b/examples/pubsub-1-basic/outputs.tf new file mode 100644 index 0000000..ddd31a3 --- /dev/null +++ b/examples/pubsub-1-basic/outputs.tf @@ -0,0 +1,4 @@ +output "pg_publications" { + description = "Current PostgreSQL publications" + value = module.tf_postgres_pubsub.postgres_publications +} \ No newline at end of file diff --git a/examples/pubsub-1-basic/providers.tf b/examples/pubsub-1-basic/providers.tf new file mode 100644 index 0000000..ca1546e --- /dev/null +++ b/examples/pubsub-1-basic/providers.tf @@ -0,0 +1,24 @@ +terraform { + required_version = ">= 1.4" + + required_providers { + postgresql = { + source = "cyrilgdn/postgresql" + version = "1.21.0" + } + + random = { + source = "hashicorp/random" + version = "3.5.1" + } + } +} + +provider "postgresql" { + host = "127.0.0.1" + port = "5432" + database = "postgres" + username = "postgres" + password = "example" + sslmode = "disable" +} diff --git a/examples/pubsub-1-basic/terrraform.tfvars b/examples/pubsub-1-basic/terrraform.tfvars new file mode 100644 index 0000000..c7d4bbd --- /dev/null +++ b/examples/pubsub-1-basic/terrraform.tfvars @@ -0,0 +1,7 @@ +pg_publications = [ + { + name = "dev_ms_auth_publication" + database = "dev_ms_auth" + tables = ["public.test"] + } + ] \ No newline at end of file diff --git a/examples/pubsub-1-basic/variables.tf b/examples/pubsub-1-basic/variables.tf new file mode 100644 index 0000000..dfd5949 --- /dev/null +++ b/examples/pubsub-1-basic/variables.tf @@ -0,0 +1,9 @@ +variable "pg_publications" { + type = list(any) + default = [] +} + +variable "pg_subscriptions" { + type = list(any) + default = [] +} diff --git a/examples/role-1-basic/locals.tf b/examples/role-1-basic/locals.tf new file mode 100644 index 0000000..c8ed3e2 --- /dev/null +++ b/examples/role-1-basic/locals.tf @@ -0,0 +1,5 @@ +locals { + roles = var.postgres_roles + # username = var.postgres_username + # password = var.postgres_password +} diff --git a/examples/role-1-basic/main.tf b/examples/role-1-basic/main.tf new file mode 100644 index 0000000..6115333 --- /dev/null +++ b/examples/role-1-basic/main.tf @@ -0,0 +1,5 @@ +module "tf_postgres_role" { + source = "../../modules/role" + roles = local.roles + default_privileges = [] +} diff --git a/examples/role-1-basic/outputs.tf b/examples/role-1-basic/outputs.tf new file mode 100644 index 0000000..cf9d695 --- /dev/null +++ b/examples/role-1-basic/outputs.tf @@ -0,0 +1,5 @@ +output "postgres_roles" { + description = "Current PostgreSQL roles" + value = module.tf_postgres_role.postgres_roles +} + diff --git a/examples/role-1-basic/providers.tf b/examples/role-1-basic/providers.tf new file mode 100644 index 0000000..a0f1e10 --- /dev/null +++ b/examples/role-1-basic/providers.tf @@ -0,0 +1,24 @@ +terraform { + required_version = ">= 1.4" + + required_providers { + postgresql = { + source = "cyrilgdn/postgresql" + version = "1.21.1-beta.1" + } + + random = { + source = "hashicorp/random" + version = "3.5.1" + } + } +} + +provider "postgresql" { + host = "127.0.0.1" + port = "5432" + database = "postgres" + username = "postgres" + password = "example" + sslmode = "disable" +} diff --git a/examples/role-1-basic/terraform.tfvars b/examples/role-1-basic/terraform.tfvars new file mode 100644 index 0000000..8be60ed --- /dev/null +++ b/examples/role-1-basic/terraform.tfvars @@ -0,0 +1,25 @@ +postgres_roles = [ + { + name = "dev_ms_auth" + login = true + }, + { + name = "dev_ms_auth_readonly" + }, + { + name = "dev_ms_payment" + login = true + }, + { + name = "tf_postgres_automation" + login = true + create_database = true + create_role = true + }, + { + name = "patroni_replication" + login = true + replication = true + connection_limit = 5 + } +] \ No newline at end of file diff --git a/examples/role-1-basic/variables.tf b/examples/role-1-basic/variables.tf new file mode 100644 index 0000000..bc53fce --- /dev/null +++ b/examples/role-1-basic/variables.tf @@ -0,0 +1,12 @@ +variable "postgres_roles" { + type = list(any) + default = [] +} + +# variable "postgres_username" { +# type = string +# } + +# variable "postgres_password" { +# type = string +# } diff --git a/examples/sample-checklist.md b/examples/sample-checklist.md new file mode 100644 index 0000000..c851ca6 --- /dev/null +++ b/examples/sample-checklist.md @@ -0,0 +1,23 @@ +# Sample Checklist + +available: + +- role +- database +- schema +- grant +- extension +- physical_replication_slot + +available, but return no changes: + +- fdw_server +- fdw_user_mapping +- publication +- subscription + +not available: + +- grant_role +- default_privilege +- replication_slot diff --git a/modules/config/main.tf b/modules/config/main.tf new file mode 100644 index 0000000..e815364 --- /dev/null +++ b/modules/config/main.tf @@ -0,0 +1,24 @@ +resource "postgresql_extension" "extensions" { + for_each = { for key, item in var.extensions : format("%s/%s", item.database, item.name) => item } + + name = each.value.name + schema = each.value.schema + version = each.value.version + database = each.value.database + drop_cascade = each.value.drop_cascade + create_cascade = each.value.create_cascade +} + +resource "postgresql_replication_slot" "slots" { + for_each = { for key, item in var.replication_slots: format("%s/%s", item.database, item.name) => item } + + name = each.value.name + plugin = each.value.plugin + database = each.value.database +} + +resource "postgresql_physical_replication_slot" "physical_slots" { + for_each = { for key, item in var.physical_replication_slots: item.name => item } + + name = each.value.name +} \ No newline at end of file diff --git a/modules/config/outputs.tf b/modules/config/outputs.tf new file mode 100644 index 0000000..72394b6 --- /dev/null +++ b/modules/config/outputs.tf @@ -0,0 +1,15 @@ +output "postgres_extensions" { + description = "List of PostgreSQL extensions" + value = [ for item in postgresql_extension.extensions : format("%s/%s", item.database, item.name) ] +} + +output "postgres_replication_slots" { + description = "List of PostgreSQL replication slots" + value = { for item in postgresql_replication_slot.slots : format("%s/%s", item.database, item.name) => item.plugin} +} + +output "postgres_physical_replication_slots" { + description = "List of PostgreSQL physical replication slots" + value = [ for item in postgresql_physical_replication_slot.physical_slots : item.name ] +} + diff --git a/modules/config/variables.tf b/modules/config/variables.tf new file mode 100644 index 0000000..a648197 --- /dev/null +++ b/modules/config/variables.tf @@ -0,0 +1,24 @@ +variable "extensions" { + type = list(object({ + name = string + schema = optional(string) + version = optional(string) + database = optional(string) + drop_cascade = optional(bool) + create_cascade = optional(bool) + })) +} + +variable "replication_slots" { + type = list(object({ + name = string + plugin = string + database = optional(string) + })) +} + +variable "physical_replication_slots" { + type = list(object({ + name = string + })) +} \ No newline at end of file diff --git a/modules/config/versions.tf b/modules/config/versions.tf new file mode 100644 index 0000000..5e4c855 --- /dev/null +++ b/modules/config/versions.tf @@ -0,0 +1,15 @@ +terraform { + required_version = ">= 1.4" + + required_providers { + postgresql = { + source = "cyrilgdn/postgresql" + version = "1.21.1-beta.1" + } + + random = { + source = "hashicorp/random" + version = "3.5.1" + } + } +} \ No newline at end of file diff --git a/modules/database/locals.tf b/modules/database/locals.tf new file mode 100644 index 0000000..e267dce --- /dev/null +++ b/modules/database/locals.tf @@ -0,0 +1,13 @@ +locals { + database_schemas = flatten([ + for db_key, database in var.databases : [ + for schema_key, schema in database.schemas: { + name = schema["name"] + owner = schema["owner"] + database = schema["database"] + drop_cascade = schema["drop_cascade"] + policies = schema["policies"] != null ? schema["policies"] : [] + } + ] if database.schemas != null + ]) +} \ No newline at end of file diff --git a/modules/database/main.tf b/modules/database/main.tf new file mode 100644 index 0000000..f33d9ce --- /dev/null +++ b/modules/database/main.tf @@ -0,0 +1,35 @@ +resource "postgresql_database" "databases" { + for_each = { for key, item in var.databases : item.name => item } + + name = each.value.name + owner = each.value.owner + tablespace_name = each.value.tablespace_name + template = each.value.template + is_template = each.value.is_template + connection_limit = each.value.connection_limit + allow_connections = each.value.allow_connections + encoding = each.value.encoding + lc_collate = each.value.lc_collate + lc_ctype = each.value.lc_ctype +} + +resource "postgresql_schema" "schemas" { + # need to be filtered by locals + for_each = { for key, item in local.database_schemas : item.name => item } + + name = each.value.name + owner = each.value.owner + database = each.value.database + drop_cascade = each.value.drop_cascade + + dynamic "policy" { + for_each = each.value.policies + content { + role = role.value["role"] + create = role.value["create"] + create_with_grant = role.value["create_with_grant"] + usage = role.value["usage"] + usage_with_grant = role.value["usage_with_grant"] + } + } +} \ No newline at end of file diff --git a/modules/database/outputs.tf b/modules/database/outputs.tf new file mode 100644 index 0000000..7d3bd8b --- /dev/null +++ b/modules/database/outputs.tf @@ -0,0 +1,9 @@ +output "postgres_databases" { + description = "List of PostgreSQL databases" + value = { for item in postgresql_database.databases : item.name => item.owner } +} + +output "postgres_schemas" { + description = "List of PostgreSQL schemas" + value = { for item in postgresql_schema.schemas : format("%s/%s", item.database, item.name) => item.owner } +} diff --git a/modules/database/variables.tf b/modules/database/variables.tf new file mode 100644 index 0000000..c3b02e1 --- /dev/null +++ b/modules/database/variables.tf @@ -0,0 +1,28 @@ +variable "databases" { + type = list(object({ + name = string + owner = optional(string) + tablespace_name = optional(string) + connection_limit = optional(number) + allow_connections = optional(bool) + is_template = optional(bool) + template = optional(string) + encoding = optional(string) + lc_collate = optional(string) + lc_ctype = optional(string) + + schemas = optional(list(object({ + name = string + owner = optional(string) + database = optional(string) + drop_cascade = optional(bool) + policies = optional(list(object({ + role = optional(string) + create = optional(bool) + create_with_grant = optional(bool) + usage = optional(bool) + usage_with_grant = optional(bool) + }))) + }))) + })) +} \ No newline at end of file diff --git a/modules/database/versions.tf b/modules/database/versions.tf new file mode 100644 index 0000000..87b1e57 --- /dev/null +++ b/modules/database/versions.tf @@ -0,0 +1,15 @@ +terraform { + required_version = ">= 1.4" + + required_providers { + postgresql = { + source = "cyrilgdn/postgresql" + version = "1.21.1-beta.1" + } + + random = { + source = "hashicorp/random" + version = "3.5.1" + } + } +} diff --git a/modules/foreign_data_wrapper/main.tf b/modules/foreign_data_wrapper/main.tf new file mode 100644 index 0000000..1058daf --- /dev/null +++ b/modules/foreign_data_wrapper/main.tf @@ -0,0 +1,19 @@ +resource "postgresql_server" "fdw_servers" { + for_each = { for key, item in var.fdw_servers : item.name => item } + + server_name = each.value.server_name + fdw_name = each.value.fdw_name + options = each.value.options + server_type = each.value.server_type + server_version = each.value.server_version + server_owner = each.value.server_owner + drop_cascade = each.value.drop_cascade +} + +resource "postgresql_user_mapping" "fdw_user_mappings" { + for_each = { for key, item in var.fdw_user_mappings : item.name => item } + + user_name = each.value.username + server_name = each.value.server_name + options = each.value.options +} diff --git a/modules/foreign_data_wrapper/outputs.tf b/modules/foreign_data_wrapper/outputs.tf new file mode 100644 index 0000000..f86918c --- /dev/null +++ b/modules/foreign_data_wrapper/outputs.tf @@ -0,0 +1,9 @@ +output "postgres_fdw_servers" { + description = "List of PostgreSQL FDW servers" + value = [ for item in postgresql_server.fdw_servers : format("%s/%s", item.server_name, item.fdw_name) ] +} + +output "postgres_fdw_user_mappings" { + description = "List of PostgreSQL FDW user mappings" + value = [ for item in postgresql_user_mapping.fdw_user_mappings : format("%s/%s", item.user_name, item.server_name) ] +} diff --git a/modules/foreign_data_wrapper/variables.tf b/modules/foreign_data_wrapper/variables.tf new file mode 100644 index 0000000..b7f0e26 --- /dev/null +++ b/modules/foreign_data_wrapper/variables.tf @@ -0,0 +1,19 @@ +variable "fdw_servers" { + type = list(object({ + server_name = string + fdw_name = string + options = optional(map(any)) + server_type = optional(string) + server_version = optional(string) + server_owner = optional(string) + drop_cascade = optional(bool) + })) +} + +variable "fdw_user_mappings" { + type = list(object({ + user_name = string + server_name = string + options = optional(map(any)) + })) +} \ No newline at end of file diff --git a/modules/foreign_data_wrapper/versions.tf b/modules/foreign_data_wrapper/versions.tf new file mode 100644 index 0000000..f39b854 --- /dev/null +++ b/modules/foreign_data_wrapper/versions.tf @@ -0,0 +1,15 @@ +terraform { + required_version = ">= 1.4" + + required_providers { + postgresql = { + source = "cyrilgdn/postgresql" + version = "1.21.0" + } + + random = { + source = "hashicorp/random" + version = "3.5.1" + } + } +} \ No newline at end of file diff --git a/modules/grant/main.tf b/modules/grant/main.tf new file mode 100644 index 0000000..6d99549 --- /dev/null +++ b/modules/grant/main.tf @@ -0,0 +1,20 @@ +resource "postgresql_grant" "grants" { + for_each = { for key, item in var.grants : item.role => item } + + role = each.value.role + database = each.value.database + schema = each.value.schema + privileges = each.value.privileges + object_type = each.value.object_type + objects = each.value.objects + columns = each.value.columns + with_grant_option = each.value.with_grant_option +} + +resource "postgresql_grant_role" "grant_roles" { + for_each = { for key, item in var.grant_roles : format("%s/%s", item.role, item.grant_role) => item } + + role = each.value.role + grant_role = each.value.grant_role + with_admin_option = each.value.with_admin_option +} diff --git a/modules/grant/outputs.tf b/modules/grant/outputs.tf new file mode 100644 index 0000000..88ebfee --- /dev/null +++ b/modules/grant/outputs.tf @@ -0,0 +1,9 @@ +output "postgres_grants" { + description = "List of PostgreSQL grants" + value = { for item in postgresql_grant.grants : format("%s/%s/%s", item.database, item.role, item.object_type) => item.privileges } +} + +output "postgres_grant_roles" { + description = "List of PostgreSQL grant roles" + value = { for item in postgresql_grant_role.grant_roles : item.role => item.grant_role } +} diff --git a/modules/grant/variables.tf b/modules/grant/variables.tf new file mode 100644 index 0000000..7c94f04 --- /dev/null +++ b/modules/grant/variables.tf @@ -0,0 +1,20 @@ +variable "grants" { + type = list(object({ + role = string + database = string + schema = string + object_type = string + privileges = list(string) + objects = optional(list(string)) + columns = optional(list(string)) + with_grant_option = optional(bool) + })) +} + +variable "grant_roles" { + type = list(object({ + role = string + grant_role = string + with_admin_option = optional(bool) + })) +} \ No newline at end of file diff --git a/modules/grant/versions.tf b/modules/grant/versions.tf new file mode 100644 index 0000000..f39b854 --- /dev/null +++ b/modules/grant/versions.tf @@ -0,0 +1,15 @@ +terraform { + required_version = ">= 1.4" + + required_providers { + postgresql = { + source = "cyrilgdn/postgresql" + version = "1.21.0" + } + + random = { + source = "hashicorp/random" + version = "3.5.1" + } + } +} \ No newline at end of file diff --git a/modules/pubsub/main.tf b/modules/pubsub/main.tf new file mode 100644 index 0000000..86358ff --- /dev/null +++ b/modules/pubsub/main.tf @@ -0,0 +1,24 @@ +resource "postgresql_publication" "publications" { + for_each = { for key, item in var.publications : item.name => item } + + name = each.value.name + tables = each.value.tables + database = each.value.database + all_tables = each.value.all_tables + owner = each.value.owner + drop_cascade = each.value.drop_cascade + publish_param = each.value.publish_param + + publish_via_partition_root_param = each.value.publish_via_partition_root_param +} + +resource "postgresql_subscription" "subscriptions" { + for_each = { for key, item in var.subscriptions : item.name => item } + + name = each.value.name + conninfo = format("host=%s port=%d dbname=%s user=%s password=%s", each.value.conninfo.host, each.value.conninfo.port, each.value.conninfo.dbname, each.value.conninfo.user, each.value.conninfo.password) + publications = each.value.publications + database = each.value.database + create_slot = each.value.create_slot + slot_name = each.value.slot_name +} \ No newline at end of file diff --git a/modules/pubsub/outputs.tf b/modules/pubsub/outputs.tf new file mode 100644 index 0000000..e9d9881 --- /dev/null +++ b/modules/pubsub/outputs.tf @@ -0,0 +1,9 @@ +output "postgres_publications" { + description = "List of PostgreSQL publications" + value = { for item in postgresql_publication.publications : format("%s/%s", item.database, item.name) => item.tables } +} + +output "postgres_subscriptions" { + description = "List of PostgreSQL subscriptions" + value = { for item in postgresql_subscription.subscriptions : format("%s/%s", item.database, item.name) => item.publications } +} diff --git a/modules/pubsub/variables.tf b/modules/pubsub/variables.tf new file mode 100644 index 0000000..5932c9f --- /dev/null +++ b/modules/pubsub/variables.tf @@ -0,0 +1,30 @@ +variable "publications" { + type = list(object({ + name = string + database = optional(string) + tables = optional(string) + all_tables = optional(bool) + owner = optional(string) + drop_cascade = optional(bool) + publish_param = optional(string) + + publish_via_partition_root_param = optional(bool) + })) +} + +variable "subscriptions" { + type = list(object({ + name = string + conninfo = object({ + host = string + port = number + dbname = string + user = string + password = string + }) + publications = list(string) + database = optional(string) + create_slot = optional(bool) + slot_name = optional(string) + })) +} \ No newline at end of file diff --git a/modules/pubsub/versions.tf b/modules/pubsub/versions.tf new file mode 100644 index 0000000..f39b854 --- /dev/null +++ b/modules/pubsub/versions.tf @@ -0,0 +1,15 @@ +terraform { + required_version = ">= 1.4" + + required_providers { + postgresql = { + source = "cyrilgdn/postgresql" + version = "1.21.0" + } + + random = { + source = "hashicorp/random" + version = "3.5.1" + } + } +} \ No newline at end of file diff --git a/modules/role/main.tf b/modules/role/main.tf new file mode 100644 index 0000000..d557659 --- /dev/null +++ b/modules/role/main.tf @@ -0,0 +1,45 @@ +resource "random_password" "password" { + for_each = { for idx, item in var.roles : item.name => item } + + length = 16 + special = true + override_special = "!#$%&*()-_=+[]{}<>:?" +} + +resource "postgresql_role" "roles" { + for_each = { for key, item in var.roles : item.name => item } + + name = each.value.name + password = random_password.password[each.value.name].result + login = each.value.login + replication = each.value.replication + superuser = each.value.superuser + create_database = each.value.create_database + create_role = each.value.create_role + inherit = each.value.inherit + connection_limit = each.value.connection_limit + search_path = each.value.search_path + valid_until = each.value.valid_until + assume_role = each.value.assume_role + roles = each.value.roles + + skip_drop_role = each.value.skip_drop_role + skip_reassign_owned = each.value.skip_reassign_owned + statement_timeout = each.value.statement_timeout + encrypted_password = each.value.encrypted_password + + bypass_row_level_security = each.value.bypass_row_level_security + + depends_on = [random_password.password] +} + +resource "postgresql_default_privileges" "default_privileges" { + for_each = { for key, item in var.default_privileges : item.name => item } + + role = each.value.role + database = each.value.database + owner = each.value.owner + object_type = each.value.object_type + privileges = each.value.privileges + schema = each.value.schema +} \ No newline at end of file diff --git a/modules/role/outputs.tf b/modules/role/outputs.tf new file mode 100644 index 0000000..b9e138e --- /dev/null +++ b/modules/role/outputs.tf @@ -0,0 +1,9 @@ +output "postgres_roles" { + description = "List of PostgreSQL roles" + value = [ for item in postgresql_role.roles : item.name ] +} + +output "postgres_default_privileges" { + description = "List of PostgreSQL default privilages" + value = { for item, key in postgresql_default_privileges.default_privileges : format("%s/%s/%s", item.database, item.role, item.schema ) => item.privileges } +} \ No newline at end of file diff --git a/modules/role/variables.tf b/modules/role/variables.tf new file mode 100644 index 0000000..81494a0 --- /dev/null +++ b/modules/role/variables.tf @@ -0,0 +1,34 @@ +variable "roles" { + type = list(object({ + name = string + login = optional(bool) + replication = optional(bool) + superuser = optional(bool) + create_database = optional(bool) + create_role = optional(bool) + inherit = optional(bool) + connection_limit = optional(number) + search_path = optional(list(string)) + valid_until = optional(string) + assume_role = optional(string) + roles = optional(list(string)) + + skip_drop_role = optional(bool) + skip_reassign_owned = optional(bool) + statement_timeout = optional(number) + encrypted_password = optional(bool) + + bypass_row_level_security = optional(bool) + })) +} + +variable "default_privileges" { + type = list(object({ + role = string + database = string + owner = string + object_type = string + privileges = string + schema = optional(string) + })) +} \ No newline at end of file diff --git a/modules/role/versions.tf b/modules/role/versions.tf new file mode 100644 index 0000000..5e4c855 --- /dev/null +++ b/modules/role/versions.tf @@ -0,0 +1,15 @@ +terraform { + required_version = ">= 1.4" + + required_providers { + postgresql = { + source = "cyrilgdn/postgresql" + version = "1.21.1-beta.1" + } + + random = { + source = "hashicorp/random" + version = "3.5.1" + } + } +} \ No newline at end of file