├── .github ├── ISSUE_TEMPLATE.md ├── SUPPORT.md └── workflows │ ├── golangci.yml │ ├── release.yml │ ├── remove-waiting-response.yml │ └── test.yml ├── .gitignore ├── .go-version ├── .golangci.yml ├── .goreleaser.yml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── examples ├── issues │ ├── 48 │ │ ├── test.tf │ │ └── test │ │ │ ├── test.tf │ │ │ └── versions.tf │ ├── 75 │ │ └── test.tf │ └── 169 │ │ ├── dev.tfrc │ │ └── test.tf └── rds │ ├── postgresql │ ├── README.md │ ├── instance.tf │ └── variables.tf │ ├── test.tf │ └── vpc │ └── vpc.tf ├── go.mod ├── go.sum ├── main.go ├── postgresql ├── GNUmakefile ├── config.go ├── config_test.go ├── data_source_helpers.go ├── data_source_postgresql_schemas.go ├── data_source_postgresql_schemas_test.go ├── data_source_postgresql_sequences.go ├── data_source_postgresql_sequences_test.go ├── data_source_postgresql_tables.go ├── data_source_postgresql_tables_test.go ├── helpers.go ├── helpers_test.go ├── model_pg_function.go ├── model_pg_function_test.go ├── provider.go ├── provider_test.go ├── proxy_driver.go ├── resource_postgresql_database.go ├── resource_postgresql_database_test.go ├── resource_postgresql_default_privileges.go ├── resource_postgresql_default_privileges_test.go ├── resource_postgresql_extension.go ├── resource_postgresql_extension_test.go ├── resource_postgresql_function.go ├── resource_postgresql_function_test.go ├── resource_postgresql_grant.go ├── resource_postgresql_grant_role.go ├── resource_postgresql_grant_role_test.go ├── resource_postgresql_grant_test.go ├── resource_postgresql_physical_replication_slot.go ├── resource_postgresql_publication.go ├── resource_postgresql_publication_test.go ├── resource_postgresql_replication_slot.go ├── resource_postgresql_replication_slot_test.go ├── resource_postgresql_role.go ├── resource_postgresql_role_test.go ├── resource_postgresql_schema.go ├── resource_postgresql_schema_test.go ├── resource_postgresql_security_label.go ├── resource_postgresql_security_label_test.go ├── resource_postgresql_server.go ├── resource_postgresql_server_test.go ├── resource_postgresql_subscription.go ├── resource_postgresql_subscription_test.go ├── resource_postgresql_user_mapping.go ├── resource_postgresql_user_mapping_test.go └── utils_test.go ├── scripts ├── changelog-links.sh ├── gofmtcheck.sh └── gogetcookie.sh ├── tests ├── build │ ├── Dockerfile │ └── dummy_seclabel │ │ ├── Makefile │ │ ├── dummy_seclabel--1.0.sql │ │ ├── dummy_seclabel.c │ │ └── dummy_seclabel.control ├── docker-compose.yml ├── switch_rds.sh ├── switch_superuser.sh ├── testacc_cleanup.sh ├── testacc_full.sh └── testacc_setup.sh └── website ├── docs ├── d │ ├── postgresql_schemas.html.markdown │ ├── postgresql_sequences.html.markdown │ └── postgresql_tables.html.markdown ├── index.html.markdown └── r │ ├── postgresql_database.html.markdown │ ├── postgresql_default_privileges.html.markdown │ ├── postgresql_extension.html.markdown │ ├── postgresql_function.html.markdown │ ├── postgresql_grant.html.markdown │ ├── postgresql_grant_role.html.markdown │ ├── postgresql_physical_replication_slot.markdown │ ├── postgresql_publication.markdown │ ├── postgresql_replication_slot.markdown │ ├── postgresql_role.html.markdown │ ├── postgresql_schema.html.markdown │ ├── postgresql_security_label.html.markdown │ ├── postgresql_server.html.markdown │ ├── postgresql_subscription.markdown │ └── postgresql_user_mapping.html.markdown └── postgresql.erb /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Hi there, 2 | 3 | Thank you for opening an issue. Please provide the following information: 4 | 5 | ### Terraform Version 6 | Run `terraform -v` to show the version. If you are not running the latest version of Terraform, please upgrade because your issue may have already been fixed. 7 | 8 | ### Affected Resource(s) 9 | Please list the resources as a list, for example: 10 | - opc_instance 11 | - opc_storage_volume 12 | 13 | If this issue appears to affect multiple resources, it may be an issue with Terraform's core, so please mention this. 14 | 15 | ### Terraform Configuration Files 16 | ```hcl 17 | # Copy-paste your Terraform configurations here - for large Terraform configs, 18 | # please use a service like Dropbox and share a link to the ZIP file. For 19 | # security, you can also encrypt the files using our GPG public key. 20 | ``` 21 | 22 | ### Debug Output 23 | Please provider a link to a GitHub Gist containing the complete debug output: https://www.terraform.io/docs/internals/debugging.html. Please do NOT paste the debug output in the issue; just paste a link to the Gist. 24 | 25 | ### Panic Output 26 | If Terraform produced a panic, please provide a link to a GitHub Gist containing the output of the `crash.log`. 27 | 28 | ### Expected Behavior 29 | What should have happened? 30 | 31 | ### Actual Behavior 32 | What actually happened? 33 | 34 | ### Steps to Reproduce 35 | Please list the steps required to reproduce the issue, for example: 36 | 1. `terraform apply` 37 | 38 | ### Important Factoids 39 | Are there anything atypical about your accounts that we should know? For example: Running in EC2 Classic? Custom version of OpenStack? Tight ACLs? 40 | 41 | ### References 42 | Are there any other GitHub issues (open or closed) or Pull Requests that should be linked here? For example: 43 | - GH-1234 44 | -------------------------------------------------------------------------------- /.github/SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Support 2 | 3 | Terraform is a mature project with a growing community. There are active, dedicated people willing to help you through various mediums. 4 | 5 | Take a look at those mediums listed at https://www.terraform.io/community.html 6 | -------------------------------------------------------------------------------- /.github/workflows/golangci.yml: -------------------------------------------------------------------------------- 1 | name: Golangci-Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | jobs: 9 | lint: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | 15 | - name: Setup Go 16 | uses: actions/setup-go@v5 17 | with: 18 | go-version: 1.23.x 19 | 20 | - run: go mod vendor 21 | 22 | - name: Run golangci-lint 23 | uses: golangci/golangci-lint-action@v6 24 | with: 25 | version: v1.61 26 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # This GitHub action can publish assets for release when a tag is created. 2 | # Currently its setup to run on any tag that matches the pattern "v*" (ie. v0.1.0). 3 | # 4 | # This uses an action (paultyng/ghaction-import-gpg) that assumes you set your 5 | # private key in the `GPG_PRIVATE_KEY` secret and passphrase in the `PASSPHRASE` 6 | # secret. If you would rather own your own GPG handling, please fork this action 7 | # or use an alternative one for key handling. 8 | # 9 | # You will need to pass the `--batch` flag to `gpg` in your signing step 10 | # in `goreleaser` to indicate this is being used in a non-interactive mode. 11 | # 12 | name: release 13 | on: 14 | push: 15 | tags: 16 | - 'v*' 17 | jobs: 18 | goreleaser: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - 22 | name: Checkout 23 | uses: actions/checkout@v4 24 | - 25 | name: Unshallow 26 | run: git fetch --prune --unshallow 27 | - 28 | name: Set up Go 29 | uses: actions/setup-go@v5 30 | with: 31 | go-version: '1.23' 32 | - 33 | name: Import GPG key 34 | id: import_gpg 35 | uses: crazy-max/ghaction-import-gpg@v6.1.0 36 | with: 37 | gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} 38 | passphrase: ${{ secrets.PASSPHRASE }} 39 | - 40 | name: Run GoReleaser 41 | uses: goreleaser/goreleaser-action@v6 42 | with: 43 | version: latest 44 | args: release --clean 45 | env: 46 | GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | -------------------------------------------------------------------------------- /.github/workflows/remove-waiting-response.yml: -------------------------------------------------------------------------------- 1 | name: Remove waiting-response label 2 | 3 | on: [issue_comment] 4 | 5 | jobs: 6 | remove_label: 7 | if: github.actor != 'cyrilgdn' 8 | runs-on: ubuntu-latest 9 | permissions: 10 | issues: write 11 | pull-requests: write 12 | steps: 13 | - uses: actions/github-script@v6 14 | with: 15 | script: | 16 | github.rest.issues.removeLabel({ 17 | issue_number: context.issue.number, 18 | owner: context.repo.owner, 19 | repo: context.repo.repo, 20 | name: ["waiting-response"] 21 | }) 22 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | pgversion: [16, 15, 14, 13, 12] 16 | 17 | env: 18 | PGVERSION: ${{ matrix.pgversion }} 19 | 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | 24 | - name: Set up Go 25 | uses: actions/setup-go@v5 26 | with: 27 | go-version: '1.23' 28 | 29 | - name: test 30 | run: make test 31 | 32 | - name: vet 33 | run: make vet 34 | 35 | - name: testacc 36 | run: make testacc 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ./*.tfstate 2 | .terraform/ 3 | .terraform.lock.hcl* 4 | *.log 5 | .*.swp 6 | tests/docker-compose.*.yml 7 | terraform-provider-postgresql 8 | -------------------------------------------------------------------------------- /.go-version: -------------------------------------------------------------------------------- 1 | 1.14.4 2 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 5m 3 | 4 | issues: 5 | exclude-rules: 6 | - linters: 7 | - errcheck 8 | text: "Error return value of `d.Set` is not checked" 9 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # Visit https://goreleaser.com for documentation on how to customize this 2 | # behavior. 3 | version: 2 4 | before: 5 | hooks: 6 | # this is just an example and not a requirement for provider building/publishing 7 | - go mod tidy 8 | builds: 9 | - env: 10 | # goreleaser does not work with CGO, it could also complicate 11 | # usage by users in CI/CD systems like Terraform Cloud where 12 | # they are unable to install libraries. 13 | - CGO_ENABLED=0 14 | mod_timestamp: '{{ .CommitTimestamp }}' 15 | flags: 16 | - -trimpath 17 | ldflags: 18 | - '-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}' 19 | goos: 20 | - freebsd 21 | - windows 22 | - linux 23 | - darwin 24 | goarch: 25 | - amd64 26 | - '386' 27 | - arm 28 | - arm64 29 | ignore: 30 | - goos: darwin 31 | goarch: '386' 32 | binary: '{{ .ProjectName }}_v{{ .Version }}' 33 | archives: 34 | - format: zip 35 | name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}' 36 | checksum: 37 | name_template: '{{ .ProjectName }}_{{ .Version }}_SHA256SUMS' 38 | algorithm: sha256 39 | signs: 40 | - artifacts: checksum 41 | args: 42 | # if you are using this is a GitHub action or some other automated pipeline, you 43 | # need to pass the batch flag to indicate it's not interactive. 44 | - "--batch" 45 | - "--local-user" 46 | - "{{ .Env.GPG_FINGERPRINT }}" # set this environment variable for your signing key 47 | - "--output" 48 | - "${signature}" 49 | - "--detach-sign" 50 | - "${artifact}" 51 | release: {} 52 | # If you want to manually examine the release before its live, uncomment this line: 53 | # draft: true 54 | changelog: 55 | disable: true 56 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | See [Github releases](https://github.com/cyrilgdn/terraform-provider-postgresql/releases/) 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TEST?=$$(go list ./...) 2 | GOFMT_FILES?=$$(find . -name '*.go') 3 | PKG_NAME=postgresql 4 | 5 | default: build 6 | 7 | build: fmtcheck 8 | go install 9 | 10 | test: fmtcheck 11 | go test $(TEST) || exit 1 12 | echo $(TEST) | \ 13 | xargs -t -n4 go test $(TESTARGS) -timeout=30s -parallel=4 14 | 15 | testacc_setup: fmtcheck 16 | @sh -c "'$(CURDIR)/tests/testacc_setup.sh'" 17 | 18 | testacc_cleanup: fmtcheck 19 | @sh -c "'$(CURDIR)/tests/testacc_cleanup.sh'" 20 | 21 | testacc: fmtcheck 22 | @sh -c "'$(CURDIR)/tests/testacc_full.sh'" 23 | 24 | vet: 25 | @echo "go vet ." 26 | @go vet $$(go list ./...) ; if [ $$? -eq 1 ]; then \ 27 | echo ""; \ 28 | echo "Vet found suspicious constructs. Please check the reported constructs"; \ 29 | echo "and fix them if necessary before submitting the code for review."; \ 30 | exit 1; \ 31 | fi 32 | 33 | fmt: 34 | gofmt -w $(GOFMT_FILES) 35 | 36 | fmtcheck: 37 | @sh -c "'$(CURDIR)/scripts/gofmtcheck.sh'" 38 | 39 | .PHONY: build test testacc vet fmt fmtcheck 40 | 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Terraform Provider for PostgreSQL 2 | ================================= 3 | 4 | This provider allows to manage with Terraform [Postgresql](https://www.postgresql.org/) objects like databases, extensions, roles, etc. 5 | 6 | It's published on the [Terraform registry](https://registry.terraform.io/providers/cyrilgdn/postgresql/latest/docs). 7 | It replaces https://github.com/hashicorp/terraform-provider-postgresql since Hashicorp stopped hosting community providers in favor of the Terraform registry. 8 | 9 | - Documentation: https://registry.terraform.io/providers/cyrilgdn/postgresql/latest/docs 10 | 11 | Requirements 12 | ------------ 13 | 14 | - [Terraform](https://www.terraform.io/downloads.html) 0.12.x 15 | - [Go](https://golang.org/doc/install) 1.16 (to build the provider plugin) 16 | 17 | Building The Provider 18 | --------------------- 19 | 20 | Clone repository to: `$GOPATH/src/github.com/cyrilgdn/terraform-provider-postgresql` 21 | 22 | ```sh 23 | $ mkdir -p $GOPATH/src/github.com/cyrilgdn; cd $GOPATH/src/github.com/cyrilgdn 24 | $ git clone git@github.com:cyrilgdn/terraform-provider-postgresql 25 | ``` 26 | 27 | Enter the provider directory and build the provider 28 | 29 | ```sh 30 | $ cd $GOPATH/src/github.com/cyrilgdn/terraform-provider-postgresql 31 | $ make build 32 | ``` 33 | 34 | Using the provider 35 | ---------------------- 36 | 37 | Usage examples can be found in the Terraform [provider documentation](https://www.terraform.io/docs/providers/postgresql/index.html) 38 | 39 | Developing the Provider 40 | --------------------------- 41 | 42 | If you wish to work on the provider, you'll first need [Go](http://www.golang.org) installed on your machine (version 1.11+ is *required*). You'll also need to correctly setup a [GOPATH](http://golang.org/doc/code.html#GOPATH), as well as adding `$GOPATH/bin` to your `$PATH`. 43 | 44 | To compile the provider, run `make build`. This will build the provider and put the provider binary in the `$GOPATH/bin` directory. 45 | 46 | ```sh 47 | $ make build 48 | ... 49 | $ $GOPATH/bin/terraform-provider-postgresql 50 | ... 51 | ``` 52 | 53 | In order to test the provider, you can simply run `make test`. 54 | 55 | ```sh 56 | $ make test 57 | ``` 58 | 59 | In order to run the full suite of Acceptance tests, run `make testacc`. 60 | 61 | *Note:* 62 | - Acceptance tests create real resources, and often cost money to run. 63 | 64 | ```sh 65 | $ make testacc 66 | ``` 67 | 68 | In order to manually run some Acceptance test locally, run the following commands: 69 | ```sh 70 | # spins up a local docker postgres container 71 | make testacc_setup 72 | 73 | # Load the needed environment variables for the tests 74 | source tests/switch_superuser.sh 75 | 76 | # Run the test(s) that you're working on as often as you want 77 | TF_LOG=INFO go test -v ./postgresql -run ^TestAccPostgresqlRole_Basic$ 78 | 79 | # cleans the env and tears down the postgres container 80 | make testacc_cleanup 81 | ``` 82 | -------------------------------------------------------------------------------- /examples/issues/169/dev.tfrc: -------------------------------------------------------------------------------- 1 | # See https://www.terraform.io/cli/config/config-file#development-overrides-for-provider-developers 2 | # Use `go build -o ./examples/issues/169/postgresql/terraform-provider-postgresql` in the project root to build the provider. 3 | # Then run terraform in this example directory. 4 | 5 | provider_installation { 6 | dev_overrides { 7 | "cyrilgdn/postgresql" = "./postgresql" 8 | } 9 | direct {} 10 | } 11 | -------------------------------------------------------------------------------- /examples/issues/169/test.tf: -------------------------------------------------------------------------------- 1 | # This tests reproduces an issue for the following error message. 2 | # ``` 3 | # terraform.tfstate 4 | # ╷ 5 | # │ Error: could not execute revoke query: pq: tuple concurrently updated 6 | # │ 7 | # │ with postgresql_grant.public_revoke_database["test3"], 8 | # │ on test.tf line 40, in resource "postgresql_grant" "public_revoke_database": 9 | # │ 40: resource "postgresql_grant" "public_revoke_database" { 10 | # │ 11 | # ╵ 12 | # ``` 13 | 14 | terraform { 15 | required_version = ">= 1.0" 16 | 17 | required_providers { 18 | postgresql = { 19 | source = "cyrilgdn/postgresql" 20 | version = ">=1.14" 21 | } 22 | } 23 | } 24 | 25 | locals { 26 | databases = toset([for idx in range(4) : format("test%d", idx)]) 27 | } 28 | 29 | provider "postgresql" { 30 | superuser = false 31 | } 32 | 33 | resource "postgresql_database" "db" { 34 | for_each = local.databases 35 | name = each.key 36 | 37 | # Use template1 instead of template0 (see https://www.postgresql.org/docs/current/manage-ag-templatedbs.html) 38 | template = "template1" 39 | } 40 | 41 | resource "postgresql_role" "demo" { 42 | name = "demo" 43 | login = true 44 | password = "Happy-Holidays!" 45 | } 46 | 47 | locals { 48 | # Create a local that is depends on postgresql_database to ensure it's created 49 | dbs = { for database in local.databases : database => postgresql_database.db[database].name } 50 | } 51 | 52 | # Revoke default accesses for PUBLIC role to the databases 53 | resource "postgresql_grant" "public_revoke_database" { 54 | for_each = local.dbs 55 | database = each.value 56 | role = "public" 57 | object_type = "database" 58 | privileges = [] 59 | 60 | with_grant_option = true 61 | } 62 | 63 | # Revoke default accesses for PUBLIC role to the public schema 64 | resource "postgresql_grant" "public_revoke_schema" { 65 | for_each = local.dbs 66 | database = each.value 67 | role = "public" 68 | schema = "public" 69 | object_type = "schema" 70 | privileges = [] 71 | 72 | with_grant_option = true 73 | } 74 | 75 | resource "postgresql_grant" "demo_db_connect" { 76 | for_each = local.dbs 77 | database = each.value 78 | role = postgresql_role.demo.name 79 | schema = "public" 80 | object_type = "database" 81 | privileges = ["CONNECT"] 82 | } 83 | -------------------------------------------------------------------------------- /examples/issues/48/test.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 0.14" 3 | required_providers { 4 | postgresql = { 5 | source = "cyrilgdn/postgresql" 6 | version = ">=1.12" 7 | } 8 | } 9 | } 10 | 11 | resource "postgresql_role" "owner" { 12 | for_each = toset([for idx in range(local.nb_dabatases) : format("test_db%d", idx)]) 13 | name = each.key 14 | } 15 | 16 | resource "postgresql_database" "db" { 17 | depends_on = [ 18 | postgresql_role.owner, 19 | ] 20 | for_each = toset([for idx in range(local.nb_dabatases) : format("test_db%d", idx)]) 21 | name = each.key 22 | owner = each.key 23 | } 24 | 25 | resource "postgresql_role" "role" { 26 | for_each = toset([for idx in range(local.nb_roles) : format("test_role%d", idx)]) 27 | name = each.key 28 | } 29 | 30 | resource "postgresql_grant" "grant" { 31 | depends_on = [ 32 | postgresql_database.db, 33 | postgresql_role.role 34 | ] 35 | 36 | for_each = { for idx in range(local.nb_roles) : idx => format("test_role%d", idx) } 37 | 38 | role = each.value 39 | database = format("test_db%d", each.key % local.nb_dabatases) 40 | schema = "public" 41 | object_type = "table" 42 | privileges = ["SELECT"] 43 | } 44 | 45 | resource "postgresql_default_privileges" "dp" { 46 | depends_on = [ 47 | postgresql_database.db, 48 | postgresql_role.role, 49 | ] 50 | 51 | for_each = { for idx in range(local.nb_roles) : idx => format("test_role%d", idx) } 52 | 53 | role = each.value 54 | database = format("test_db%d", each.key % local.nb_dabatases) 55 | owner = format("test_db%d", each.key % local.nb_dabatases) 56 | schema = "public" 57 | object_type = "table" 58 | privileges = ["SELECT"] 59 | } 60 | 61 | locals { 62 | nb_dabatases = 3 63 | nb_roles = 15 * local.nb_dabatases 64 | } 65 | 66 | module "test" { 67 | for_each = toset([for idx in range(20) : format("test%d", idx)]) 68 | source = "./test" 69 | role = each.key 70 | } 71 | -------------------------------------------------------------------------------- /examples/issues/48/test/test.tf: -------------------------------------------------------------------------------- 1 | variable "role" {} 2 | variable "grant_roles" { 3 | type = map 4 | default = { 5 | user1 = ["SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE", "REFERENCES", "TRIGGER"], 6 | user2 = ["SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE", "REFERENCES", "TRIGGER"], 7 | user3 = ["SELECT"] 8 | } 9 | } 10 | 11 | resource "postgresql_role" "owner" { 12 | name = "owner_${var.role}" 13 | } 14 | 15 | resource "postgresql_role" "role" { 16 | name = var.role 17 | } 18 | 19 | resource "postgresql_database" "db" { 20 | name = var.role 21 | owner = postgresql_role.role.name 22 | } 23 | 24 | resource "postgresql_grant" "g" { 25 | for_each = var.grant_roles 26 | 27 | role = postgresql_role.role.name 28 | database = postgresql_database.db.name 29 | schema = "public" 30 | object_type = "table" 31 | privileges = each.value 32 | } 33 | 34 | resource "postgresql_default_privileges" "dp" { 35 | for_each = var.grant_roles 36 | 37 | role = postgresql_role.role.name 38 | database = postgresql_database.db.name 39 | owner = postgresql_role.owner.name 40 | schema = "public" 41 | object_type = "table" 42 | privileges = each.value 43 | } 44 | -------------------------------------------------------------------------------- /examples/issues/48/test/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | postgresql = { 4 | source = "cyrilgdn/postgresql" 5 | version = ">=1.12" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/issues/75/test.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | postgresql = { 4 | source = "cyrilgdn/postgresql" 5 | version = ">=1.12" 6 | } 7 | } 8 | } 9 | 10 | data "terraform_remote_state" "rds" { 11 | backend = "local" 12 | 13 | config = { 14 | path = "../../rds/terraform.tfstate" 15 | } 16 | } 17 | 18 | provider "postgresql" { 19 | host = data.terraform_remote_state.rds.outputs.db.address 20 | port = data.terraform_remote_state.rds.outputs.db.port 21 | database = "postgres" 22 | username = data.terraform_remote_state.rds.outputs.db.username 23 | password = data.terraform_remote_state.rds.outputs.db.password 24 | sslmode = "require" 25 | superuser = false 26 | } 27 | 28 | resource "postgresql_database" "test" { 29 | name = "test" 30 | } 31 | 32 | resource "postgresql_role" "test" { 33 | name = "test" 34 | } 35 | 36 | resource "postgresql_role" "test_readonly" { 37 | name = "test_readonly" 38 | login = true 39 | password = "toto" 40 | } 41 | 42 | resource "postgresql_grant" "grant_ro_sequence" { 43 | database = postgresql_database.test.name 44 | role = postgresql_role.test_readonly.name 45 | schema = "public" 46 | object_type = "sequence" 47 | privileges = ["USAGE", "SELECT"] 48 | } 49 | 50 | resource "postgresql_grant" "grant_ro_tables" { 51 | database = postgresql_database.test.name 52 | role = postgresql_role.test_readonly.name 53 | schema = "public" 54 | object_type = "table" 55 | privileges = ["SELECT"] 56 | } 57 | 58 | resource "postgresql_default_privileges" "alter_ro_tables" { 59 | database = postgresql_database.test.name 60 | owner = postgresql_role.test.name 61 | role = postgresql_role.test_readonly.name 62 | schema = "public" 63 | object_type = "table" 64 | privileges = ["SELECT"] 65 | } 66 | 67 | resource "postgresql_default_privileges" "alter_ro_sequence" { 68 | database = postgresql_database.test.name 69 | owner = postgresql_role.test.name 70 | role = postgresql_role.test_readonly.name 71 | schema = "public" 72 | object_type = "sequence" 73 | privileges = ["USAGE", "SELECT"] 74 | } 75 | 76 | resource "postgresql_grant" "revoke_public" { 77 | database = postgresql_database.test.name 78 | role = "public" 79 | schema = "public" 80 | object_type = "schema" 81 | privileges = [] 82 | 83 | with_grant_option = true 84 | } 85 | -------------------------------------------------------------------------------- /examples/rds/postgresql/README.md: -------------------------------------------------------------------------------- 1 | #RDS instance 2 | 3 | This module creates an RDS Postgresql database instance to test the Terraform provider. 4 | 5 | :warning: **This creates a wide publicly accessible database (with a default hardcoded password).** 6 | :warning: **This is only for tests purpose and should be destroyed as soon as possible.** 7 | -------------------------------------------------------------------------------- /examples/rds/postgresql/instance.tf: -------------------------------------------------------------------------------- 1 | module "vpc" { 2 | source = "../vpc/" 3 | name = var.name 4 | } 5 | 6 | resource "aws_db_subnet_group" "public" { 7 | name = var.name 8 | subnet_ids = module.vpc.public_subnet_ids 9 | } 10 | 11 | resource "aws_security_group" "postgresql" { 12 | name = "${var.name}-postgresql" 13 | vpc_id = module.vpc.id 14 | 15 | ingress { 16 | from_port = 5432 17 | to_port = 5432 18 | protocol = "tcp" 19 | cidr_blocks = ["0.0.0.0/0"] 20 | } 21 | } 22 | 23 | resource "aws_db_instance" "db" { 24 | identifier = var.name 25 | engine = "postgres" 26 | 27 | engine_version = var.engine_version 28 | auto_minor_version_upgrade = false 29 | 30 | instance_class = var.instance_class 31 | 32 | allocated_storage = 20 33 | 34 | username = var.username 35 | password = var.password 36 | 37 | skip_final_snapshot = true 38 | 39 | vpc_security_group_ids = [ 40 | aws_security_group.postgresql.id, 41 | ] 42 | 43 | db_subnet_group_name = aws_db_subnet_group.public.name 44 | multi_az = false 45 | 46 | publicly_accessible = true 47 | } 48 | 49 | output "db" { 50 | value = aws_db_instance.db 51 | } 52 | -------------------------------------------------------------------------------- /examples/rds/postgresql/variables.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | description = "Name of resources (vpc, db instance, ...)" 3 | default = "test-cyrildgn" 4 | } 5 | 6 | variable "engine_version" { 7 | default = "13.2" 8 | } 9 | 10 | variable "instance_class" { 11 | default = "db.t3.micro" 12 | } 13 | 14 | variable "username" { 15 | default = "postgres" 16 | } 17 | 18 | variable "password" { 19 | default = "postgrespwd" 20 | } 21 | -------------------------------------------------------------------------------- /examples/rds/test.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | } 6 | postgresql = { 7 | source = "cyrilgdn/postgresql" 8 | version = "1.12.0" 9 | } 10 | } 11 | required_version = ">= 0.14.0" 12 | } 13 | 14 | module "db_instance" { 15 | source = "./postgresql" 16 | } 17 | 18 | provider "postgresql" { 19 | host = module.db_instance.db.address 20 | port = module.db_instance.db.port 21 | database = "postgres" 22 | username = module.db_instance.db.username 23 | password = module.db_instance.db.password 24 | sslmode = "require" 25 | superuser = false 26 | } 27 | 28 | resource "postgresql_role" "test_role" { 29 | name = "test_role" 30 | login = true 31 | password = "test1234" 32 | } 33 | 34 | output "db_address" { 35 | value = module.db_instance.db.address 36 | } 37 | -------------------------------------------------------------------------------- /examples/rds/vpc/vpc.tf: -------------------------------------------------------------------------------- 1 | variable "name" {} 2 | 3 | variable "cidr_block" { 4 | default = "192.168.1.0/24" 5 | } 6 | 7 | variable "availability_zone" { 8 | default = "eu-central-1a" 9 | } 10 | 11 | data "aws_availability_zones" "available" {} 12 | 13 | resource aws_vpc "this" { 14 | cidr_block = var.cidr_block 15 | enable_dns_hostnames = true 16 | enable_dns_support = true 17 | 18 | tags = { 19 | Name = var.name 20 | } 21 | } 22 | 23 | resource aws_subnet "public" { 24 | count = length(data.aws_availability_zones.available.names) 25 | 26 | vpc_id = aws_vpc.this.id 27 | cidr_block = cidrsubnet( 28 | cidrsubnet(var.cidr_block, 1, 1), 2, count.index, 29 | ) 30 | 31 | availability_zone = data.aws_availability_zones.available.names[count.index] 32 | 33 | map_public_ip_on_launch = true 34 | } 35 | 36 | resource aws_internet_gateway "this" { 37 | vpc_id = aws_vpc.this.id 38 | } 39 | 40 | resource aws_route_table "public_subnets" { 41 | vpc_id = aws_vpc.this.id 42 | } 43 | 44 | resource aws_route "default_via_internet_gateway" { 45 | route_table_id = aws_route_table.public_subnets.id 46 | destination_cidr_block = "0.0.0.0/0" 47 | gateway_id = aws_internet_gateway.this.id 48 | } 49 | 50 | resource aws_route_table_association "public_via_internet_gateway" { 51 | count = length(aws_subnet.public) 52 | 53 | subnet_id = element(aws_subnet.public.*.id, count.index) 54 | route_table_id = aws_route_table.public_subnets.id 55 | } 56 | 57 | resource aws_main_route_table_association "this" { 58 | vpc_id = aws_vpc.this.id 59 | route_table_id = aws_route_table.public_subnets.id 60 | } 61 | 62 | output "id" { 63 | value = aws_vpc.this.id 64 | } 65 | 66 | output "public_subnet_ids" { 67 | value = aws_subnet.public.*.id 68 | } 69 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/terraform-providers/terraform-provider-postgresql 2 | 3 | go 1.23 4 | 5 | toolchain go1.23.2 6 | 7 | require ( 8 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 9 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0 10 | github.com/aws/aws-sdk-go-v2 v1.20.0 11 | github.com/aws/aws-sdk-go-v2/config v1.18.32 12 | github.com/aws/aws-sdk-go-v2/credentials v1.13.31 13 | github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.2.12 14 | github.com/aws/aws-sdk-go-v2/service/sts v1.21.1 15 | github.com/blang/semver v3.5.1+incompatible 16 | github.com/hashicorp/terraform-plugin-sdk/v2 v2.26.1 17 | github.com/lib/pq v1.10.9 18 | github.com/sean-/postgresql-acl v0.0.0-20161225120419-d10489e5d217 19 | github.com/stretchr/testify v1.9.0 20 | gocloud.dev v0.34.0 21 | golang.org/x/net v0.26.0 22 | golang.org/x/oauth2 v0.10.0 23 | google.golang.org/api v0.134.0 24 | ) 25 | 26 | require ( 27 | cloud.google.com/go/compute v1.23.0 // indirect 28 | cloud.google.com/go/compute/metadata v0.2.3 // indirect 29 | contrib.go.opencensus.io/integrations/ocsql v0.1.7 // indirect 30 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 // indirect 31 | github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect 32 | github.com/GoogleCloudPlatform/cloudsql-proxy v1.33.9 // indirect 33 | github.com/agext/levenshtein v1.2.3 // indirect 34 | github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect 35 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.7 // indirect 36 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.37 // indirect 37 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.31 // indirect 38 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.38 // indirect 39 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.31 // indirect 40 | github.com/aws/aws-sdk-go-v2/service/sso v1.13.1 // indirect 41 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.1 // indirect 42 | github.com/aws/smithy-go v1.14.0 // indirect 43 | github.com/davecgh/go-spew v1.1.1 // indirect 44 | github.com/fatih/color v1.15.0 // indirect 45 | github.com/golang-jwt/jwt/v5 v5.2.1 // indirect 46 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 47 | github.com/golang/protobuf v1.5.3 // indirect 48 | github.com/google/go-cmp v0.5.9 // indirect 49 | github.com/google/s2a-go v0.1.4 // indirect 50 | github.com/google/uuid v1.6.0 // indirect 51 | github.com/google/wire v0.5.0 // indirect 52 | github.com/googleapis/enterprise-certificate-proxy v0.2.5 // indirect 53 | github.com/googleapis/gax-go/v2 v2.12.0 // indirect 54 | github.com/hashicorp/errwrap v1.1.0 // indirect 55 | github.com/hashicorp/go-checkpoint v0.5.0 // indirect 56 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 57 | github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 // indirect 58 | github.com/hashicorp/go-hclog v1.5.0 // indirect 59 | github.com/hashicorp/go-multierror v1.1.1 // indirect 60 | github.com/hashicorp/go-plugin v1.4.10 // indirect 61 | github.com/hashicorp/go-uuid v1.0.3 // indirect 62 | github.com/hashicorp/go-version v1.6.0 // indirect 63 | github.com/hashicorp/hc-install v0.5.0 // indirect 64 | github.com/hashicorp/hcl/v2 v2.17.0 // indirect 65 | github.com/hashicorp/logutils v1.0.0 // indirect 66 | github.com/hashicorp/terraform-exec v0.18.1 // indirect 67 | github.com/hashicorp/terraform-json v0.16.0 // indirect 68 | github.com/hashicorp/terraform-plugin-go v0.15.0 // indirect 69 | github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect 70 | github.com/hashicorp/terraform-registry-address v0.2.1 // indirect 71 | github.com/hashicorp/terraform-svchost v0.1.1 // indirect 72 | github.com/hashicorp/yamux v0.1.1 // indirect 73 | github.com/kylelemons/godebug v1.1.0 // indirect 74 | github.com/mattn/go-colorable v0.1.13 // indirect 75 | github.com/mattn/go-isatty v0.0.19 // indirect 76 | github.com/mitchellh/copystructure v1.2.0 // indirect 77 | github.com/mitchellh/go-testing-interface v1.14.1 // indirect 78 | github.com/mitchellh/go-wordwrap v1.0.1 // indirect 79 | github.com/mitchellh/mapstructure v1.5.0 // indirect 80 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 81 | github.com/oklog/run v1.1.0 // indirect 82 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect 83 | github.com/pmezard/go-difflib v1.0.0 // indirect 84 | github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect 85 | github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect 86 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 87 | github.com/zclconf/go-cty v1.13.2 // indirect 88 | go.opencensus.io v0.24.0 // indirect 89 | go.uber.org/atomic v1.11.0 // indirect 90 | go.uber.org/multierr v1.11.0 // indirect 91 | go.uber.org/zap v1.24.0 // indirect 92 | golang.org/x/crypto v0.31.0 // indirect 93 | golang.org/x/mod v0.17.0 // indirect 94 | golang.org/x/sys v0.28.0 // indirect 95 | golang.org/x/text v0.21.0 // indirect 96 | golang.org/x/time v0.3.0 // indirect 97 | google.golang.org/appengine v1.6.7 // indirect 98 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230731193218-e0aa005b6bdf // indirect 99 | google.golang.org/grpc v1.57.0 // indirect 100 | google.golang.org/protobuf v1.33.0 // indirect 101 | gopkg.in/yaml.v3 v3.0.1 // indirect 102 | ) 103 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/hashicorp/terraform-plugin-sdk/v2/plugin" 5 | "github.com/terraform-providers/terraform-provider-postgresql/postgresql" 6 | ) 7 | 8 | func main() { 9 | plugin.Serve(&plugin.ServeOpts{ 10 | ProviderFunc: postgresql.Provider}) 11 | } 12 | -------------------------------------------------------------------------------- /postgresql/GNUmakefile: -------------------------------------------------------------------------------- 1 | # env TESTARGS='-run TestAccPostgresqlSchema_AddPolicy' TF_LOG=warn make test 2 | # 3 | # NOTE: As of PostgreSQL 9.6.1 the -test.parallel=1 is required when 4 | # performing `DROP ROLE`-related actions. This behavior and requirement 5 | # may change in the future and is likely not required when doing 6 | # non-delete related operations. But for now it is. 7 | 8 | PGVERSION?=96 9 | POSTGRES?=$(wildcard /usr/local/bin/postgres /opt/local/lib/postgresql$(PGVERSION)/bin/postgres) 10 | PSQL?=$(wildcard /usr/local/bin/psql /opt/local/lib/postgresql$(PGVERSION)/bin/psql) 11 | INITDB?=$(wildcard /usr/local/bin/initdb /opt/local/lib/postgresql$(PGVERSION)/bin/initdb) 12 | 13 | PGDATA?=$(GOPATH)/src/github.com/terraform-providers/terraform-provider-postgresql/data 14 | 15 | initdb:: 16 | -cat /dev/urandom | strings | grep -o '[[:alnum:]]' | head -n 32 | tr -d '\n' > pwfile 17 | $(INITDB) --no-locale -U postgres -A md5 --pwfile=pwfile -D $(PGDATA) 18 | 19 | startdb:: 20 | 2>&1 \ 21 | $(POSTGRES) \ 22 | -D $(PGDATA) \ 23 | -c log_connections=on \ 24 | -c log_disconnections=on \ 25 | -c log_duration=on \ 26 | -c log_statement=all \ 27 | | tee postgresql.log 28 | 29 | cleandb:: 30 | rm -rf $(PGDATA) 31 | rm -f pwfile 32 | 33 | freshdb:: cleandb initdb startdb 34 | 35 | test:: 36 | 2>&1 PGSSLMODE=disable PGHOST=/tmp PGUSER=postgres PGPASSWORD="`cat pwfile`" make -C ../ testacc TEST=./postgresql | tee test.log 37 | 38 | psql:: 39 | env PGPASSWORD="`cat pwfile`" $(PSQL) -E postgres postgres 40 | -------------------------------------------------------------------------------- /postgresql/config_test.go: -------------------------------------------------------------------------------- 1 | package postgresql 2 | 3 | import ( 4 | "reflect" 5 | "sort" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/blang/semver" 10 | ) 11 | 12 | func TestConfigConnParams(t *testing.T) { 13 | var tests = []struct { 14 | input *Config 15 | want []string 16 | }{ 17 | {&Config{Scheme: "postgres", SSLMode: "require", ConnectTimeoutSec: 10}, []string{"connect_timeout=10", "sslmode=require"}}, 18 | {&Config{Scheme: "postgres", SSLMode: "disable"}, []string{"connect_timeout=0", "sslmode=disable"}}, 19 | {&Config{Scheme: "awspostgres", ConnectTimeoutSec: 10}, []string{}}, 20 | {&Config{Scheme: "awspostgres", SSLMode: "disable"}, []string{}}, 21 | {&Config{ExpectedVersion: semver.MustParse("9.0.0"), ApplicationName: "Terraform provider"}, []string{"fallback_application_name=Terraform+provider"}}, 22 | {&Config{ExpectedVersion: semver.MustParse("8.0.0"), ApplicationName: "Terraform provider"}, []string{}}, 23 | {&Config{SSLClientCert: &ClientCertificateConfig{CertificatePath: "/path/to/public-certificate.pem", KeyPath: "/path/to/private-key.pem"}}, []string{"sslcert=%2Fpath%2Fto%2Fpublic-certificate.pem", "sslkey=%2Fpath%2Fto%2Fprivate-key.pem"}}, 24 | {&Config{SSLRootCertPath: "/path/to/root.pem"}, []string{"sslrootcert=%2Fpath%2Fto%2Froot.pem"}}, 25 | } 26 | 27 | for _, test := range tests { 28 | 29 | connParams := test.input.connParams() 30 | 31 | sort.Strings(connParams) 32 | sort.Strings(test.want) 33 | 34 | if !reflect.DeepEqual(connParams, test.want) { 35 | t.Errorf("Config.connParams(%+v) returned %#v, want %#v", test.input, connParams, test.want) 36 | } 37 | 38 | } 39 | } 40 | 41 | func TestConfigConnStr(t *testing.T) { 42 | var tests = []struct { 43 | input *Config 44 | wantDbURL string 45 | wantDbParams []string 46 | }{ 47 | {&Config{Scheme: "postgres", Host: "localhost", Port: 5432, Username: "postgres_user", Password: "postgres_password", SSLMode: "disable"}, "postgres://postgres_user:postgres_password@localhost:5432/postgres", []string{"connect_timeout=0", "sslmode=disable"}}, 48 | {&Config{Scheme: "postgres", Host: "localhost", Port: 5432, Username: "spaced user", Password: "spaced password", SSLMode: "disable"}, "postgres://spaced%20user:spaced%20password@localhost:5432/postgres", []string{"connect_timeout=0", "sslmode=disable"}}, 49 | } 50 | 51 | for _, test := range tests { 52 | 53 | connStr := test.input.connStr("postgres") 54 | 55 | splitConnStr := strings.Split(connStr, "?") 56 | 57 | if splitConnStr[0] != test.wantDbURL { 58 | t.Errorf("Config.connStr(%+v) returned %#v, want %#v", test.input, splitConnStr[0], test.wantDbURL) 59 | } 60 | 61 | connParams := strings.Split(splitConnStr[1], "&") 62 | 63 | sort.Strings(connParams) 64 | sort.Strings(test.wantDbParams) 65 | 66 | if !reflect.DeepEqual(connParams, test.wantDbParams) { 67 | t.Errorf("Config.connStr(%+v) returned %#v, want %#v", test.input, connParams, test.wantDbParams) 68 | } 69 | 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /postgresql/data_source_helpers.go: -------------------------------------------------------------------------------- 1 | package postgresql 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 8 | ) 9 | 10 | const ( 11 | queryConcatKeywordWhere = "WHERE" 12 | queryConcatKeywordAnd = "AND" 13 | queryArrayKeywordAny = "ANY" 14 | queryArrayKeywordAll = "ALL" 15 | likePatternQuery = "LIKE" 16 | notLikePatternQuery = "NOT LIKE" 17 | regexPatternQuery = "~" 18 | ) 19 | 20 | func applyPatternMatchingToQuery(patternMatchingTarget string, d *schema.ResourceData) []string { 21 | likeAnyPatterns := d.Get("like_any_patterns").([]interface{}) 22 | likeAllPatterns := d.Get("like_all_patterns").([]interface{}) 23 | notLikeAllPatterns := d.Get("not_like_all_patterns").([]interface{}) 24 | regexPattern := d.Get("regex_pattern").(string) 25 | 26 | filters := []string{} 27 | if len(likeAnyPatterns) > 0 { 28 | filters = append(filters, generatePatternMatchingString(patternMatchingTarget, likePatternQuery, generatePatternArrayString(likeAnyPatterns, queryArrayKeywordAny))) 29 | } 30 | if len(likeAllPatterns) > 0 { 31 | filters = append(filters, generatePatternMatchingString(patternMatchingTarget, likePatternQuery, generatePatternArrayString(likeAllPatterns, queryArrayKeywordAll))) 32 | } 33 | if len(notLikeAllPatterns) > 0 { 34 | filters = append(filters, generatePatternMatchingString(patternMatchingTarget, notLikePatternQuery, generatePatternArrayString(notLikeAllPatterns, queryArrayKeywordAll))) 35 | } 36 | if regexPattern != "" { 37 | filters = append(filters, generatePatternMatchingString(patternMatchingTarget, regexPatternQuery, fmt.Sprintf("'%s'", regexPattern))) 38 | } 39 | 40 | return filters 41 | } 42 | 43 | func generatePatternMatchingString(patternMatchingTarget string, additionalQueryKeyword string, pattern string) string { 44 | patternMatchingFilter := fmt.Sprintf("%s %s %s", patternMatchingTarget, additionalQueryKeyword, pattern) 45 | 46 | return patternMatchingFilter 47 | } 48 | 49 | func applyTypeMatchingToQuery(objectKeyword string, objects []interface{}) string { 50 | var typeFilter string 51 | if len(objects) > 0 { 52 | typeFilter = fmt.Sprintf("%s = %s", objectKeyword, generatePatternArrayString(objects, queryArrayKeywordAny)) 53 | } 54 | 55 | return typeFilter 56 | } 57 | 58 | func generatePatternArrayString(patterns []interface{}, queryArrayKeyword string) string { 59 | formattedPatterns := []string{} 60 | 61 | for _, pattern := range patterns { 62 | formattedPatterns = append(formattedPatterns, fmt.Sprintf("'%s'", pattern.(string))) 63 | } 64 | return fmt.Sprintf("%s (array[%s])", queryArrayKeyword, strings.Join(formattedPatterns, ",")) 65 | } 66 | 67 | func finalizeQueryWithFilters(query string, queryConcatKeyword string, filters []string) string { 68 | if len(filters) > 0 { 69 | query = fmt.Sprintf("%s %s %s", query, queryConcatKeyword, strings.Join(filters, " AND ")) 70 | } 71 | 72 | return query 73 | } 74 | -------------------------------------------------------------------------------- /postgresql/data_source_postgresql_schemas.go: -------------------------------------------------------------------------------- 1 | package postgresql 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 9 | ) 10 | 11 | var schemaQueries = map[string]string{ 12 | "query_include_system_schemas": ` 13 | SELECT schema_name 14 | FROM information_schema.schemata 15 | `, 16 | "query_exclude_system_schemas": ` 17 | SELECT schema_name 18 | FROM information_schema.schemata 19 | WHERE schema_name NOT LIKE 'pg_%' 20 | AND schema_name <> 'information_schema' 21 | `, 22 | } 23 | 24 | const schemaPatternMatchingTarget = "schema_name" 25 | 26 | func dataSourcePostgreSQLDatabaseSchemas() *schema.Resource { 27 | return &schema.Resource{ 28 | Read: PGResourceFunc(dataSourcePostgreSQLSchemasRead), 29 | Schema: map[string]*schema.Schema{ 30 | "database": { 31 | Type: schema.TypeString, 32 | Required: true, 33 | ForceNew: true, 34 | Description: "The PostgreSQL database which will be queried for schema names", 35 | }, 36 | "include_system_schemas": { 37 | Type: schema.TypeBool, 38 | Default: false, 39 | Optional: true, 40 | Description: "Determines whether to include system schemas (pg_ prefix and information_schema). 'public' will always be included.", 41 | }, 42 | "like_any_patterns": { 43 | Type: schema.TypeList, 44 | Optional: true, 45 | Elem: &schema.Schema{Type: schema.TypeString}, 46 | MinItems: 0, 47 | Description: "Expression(s) which will be pattern matched in the query using the PostgreSQL LIKE ANY operator", 48 | }, 49 | "like_all_patterns": { 50 | Type: schema.TypeList, 51 | Optional: true, 52 | Elem: &schema.Schema{Type: schema.TypeString}, 53 | MinItems: 0, 54 | Description: "Expression(s) which will be pattern matched in the query using the PostgreSQL LIKE ALL operator", 55 | }, 56 | "not_like_all_patterns": { 57 | Type: schema.TypeList, 58 | Optional: true, 59 | Elem: &schema.Schema{Type: schema.TypeString}, 60 | MinItems: 0, 61 | Description: "Expression(s) which will be pattern matched in the query using the PostgreSQL NOT LIKE ALL operator", 62 | }, 63 | "regex_pattern": { 64 | Type: schema.TypeString, 65 | Optional: true, 66 | Description: "Expression which will be pattern matched in the query using the PostgreSQL ~ (regular expression match) operator", 67 | }, 68 | "schemas": { 69 | Type: schema.TypeSet, 70 | Computed: true, 71 | Elem: &schema.Schema{Type: schema.TypeString}, 72 | Set: schema.HashString, 73 | Description: "The list of PostgreSQL schemas retrieved by this data source", 74 | }, 75 | }, 76 | } 77 | } 78 | 79 | func dataSourcePostgreSQLSchemasRead(db *DBConnection, d *schema.ResourceData) error { 80 | database := d.Get("database").(string) 81 | 82 | txn, err := startTransaction(db.client, database) 83 | if err != nil { 84 | return err 85 | } 86 | defer deferredRollback(txn) 87 | 88 | includeSystemSchemas := d.Get("include_system_schemas").(bool) 89 | 90 | var query string 91 | var queryConcatKeyword string 92 | if includeSystemSchemas { 93 | query = schemaQueries["query_include_system_schemas"] 94 | queryConcatKeyword = queryConcatKeywordWhere 95 | } else { 96 | query = schemaQueries["query_exclude_system_schemas"] 97 | queryConcatKeyword = queryConcatKeywordAnd 98 | } 99 | 100 | query = applySchemaDataSourceQueryFilters(query, queryConcatKeyword, d) 101 | 102 | rows, err := txn.Query(query) 103 | if err != nil { 104 | return err 105 | } 106 | defer rows.Close() 107 | 108 | schemas := []string{} 109 | for rows.Next() { 110 | var schema string 111 | 112 | if err = rows.Scan(&schema); err != nil { 113 | return fmt.Errorf("could not scan schema name for database: %w", err) 114 | } 115 | schemas = append(schemas, schema) 116 | } 117 | 118 | d.Set("schemas", stringSliceToSet(schemas)) 119 | d.SetId(generateDataSourceSchemasID(d, database)) 120 | 121 | return nil 122 | } 123 | 124 | func generateDataSourceSchemasID(d *schema.ResourceData, databaseName string) string { 125 | return strings.Join([]string{ 126 | databaseName, strconv.FormatBool(d.Get("include_system_schemas").(bool)), 127 | generatePatternArrayString(d.Get("like_any_patterns").([]interface{}), queryArrayKeywordAny), 128 | generatePatternArrayString(d.Get("like_all_patterns").([]interface{}), queryArrayKeywordAll), 129 | generatePatternArrayString(d.Get("not_like_all_patterns").([]interface{}), queryArrayKeywordAll), 130 | d.Get("regex_pattern").(string), 131 | }, "_") 132 | } 133 | 134 | func applySchemaDataSourceQueryFilters(query string, queryConcatKeyword string, d *schema.ResourceData) string { 135 | filters := []string{} 136 | filters = append(filters, applyPatternMatchingToQuery(schemaPatternMatchingTarget, d)...) 137 | 138 | return finalizeQueryWithFilters(query, queryConcatKeyword, filters) 139 | } 140 | -------------------------------------------------------------------------------- /postgresql/data_source_postgresql_schemas_test.go: -------------------------------------------------------------------------------- 1 | package postgresql 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" 8 | ) 9 | 10 | func TestAccPostgresqlDataSourceSchemas(t *testing.T) { 11 | skipIfNotAcc(t) 12 | 13 | // Create the database outside of resource.Test 14 | // because we need to create test schemas. 15 | dbSuffix, teardown := setupTestDatabase(t, true, true) 16 | defer teardown() 17 | 18 | //Note that the db will also include 'test_schema' and 'dev_schema' from setupTestDatabase along with these schemas. 19 | //In addition, the db includes 4 system schemas: 'information_schema', 'pg_catalog', 'pg_toast' and 'public' 20 | //along with a variable number of 'pg_temp_*' and 'pg_toast_temp_*' temporary system schemas. 21 | //'public' is always included in the output regardless of the 'include_system_schemas' setting. 22 | schemas := []string{"test_schema1", "test_schema2", "test_exp", "exp_test", "test_pg"} 23 | createTestSchemas(t, dbSuffix, schemas, "") 24 | 25 | dbName, _ := getTestDBNames(dbSuffix) 26 | 27 | testAccPostgresqlDataSourceSchemasDatabaseConfig := generateDataSourceSchemasConfig(dbName) 28 | 29 | resource.Test(t, resource.TestCase{ 30 | PreCheck: func() { testAccPreCheck(t) }, 31 | Providers: testAccProviders, 32 | Steps: []resource.TestStep{ 33 | { 34 | Config: testAccPostgresqlDataSourceSchemasDatabaseConfig, 35 | Check: resource.ComposeTestCheckFunc( 36 | resource.TestCheckResourceAttr("data.postgresql_schemas.system_false", "schemas.#", "8"), 37 | resource.TestCheckResourceAttr("data.postgresql_schemas.no_match", "schemas.#", "0"), 38 | resource.TestCheckResourceAttr("data.postgresql_schemas.system_false_like_exp", "schemas.#", "2"), 39 | resource.TestCheckTypeSetElemAttr("data.postgresql_schemas.system_false_like_exp", "schemas.*", "test_exp"), 40 | resource.TestCheckTypeSetElemAttr("data.postgresql_schemas.system_false_like_exp", "schemas.*", "exp_test"), 41 | resource.TestCheckResourceAttr("data.postgresql_schemas.system_true_like_exp", "schemas.#", "2"), 42 | resource.TestCheckResourceAttr("data.postgresql_schemas.like_pg", "schemas.#", "0"), 43 | resource.TestCheckResourceAttr("data.postgresql_schemas.system_false_like_pg", "schemas.#", "0"), 44 | resource.TestCheckResourceAttr("data.postgresql_schemas.system_false_like_pg_double_wildcard", "schemas.#", "1"), 45 | resource.TestCheckTypeSetElemAttr("data.postgresql_schemas.system_false_like_pg_double_wildcard", "schemas.*", "test_pg"), 46 | resource.TestCheckResourceAttr("data.postgresql_schemas.system_true_like_information_schema", "schemas.#", "1"), 47 | resource.TestCheckTypeSetElemAttr("data.postgresql_schemas.system_true_like_information_schema", "schemas.*", "information_schema"), 48 | resource.TestCheckResourceAttr("data.postgresql_schemas.like_test_schema", "schemas.#", "3"), 49 | resource.TestCheckTypeSetElemAttr("data.postgresql_schemas.like_test_schema", "schemas.*", "test_schema"), 50 | resource.TestCheckTypeSetElemAttr("data.postgresql_schemas.like_test_schema", "schemas.*", "test_schema1"), 51 | resource.TestCheckTypeSetElemAttr("data.postgresql_schemas.like_test_schema", "schemas.*", "test_schema2"), 52 | resource.TestCheckResourceAttr("data.postgresql_schemas.regex_test_schema", "schemas.#", "3"), 53 | resource.TestCheckTypeSetElemAttr("data.postgresql_schemas.regex_test_schema", "schemas.*", "test_schema"), 54 | resource.TestCheckTypeSetElemAttr("data.postgresql_schemas.regex_test_schema", "schemas.*", "test_schema1"), 55 | resource.TestCheckTypeSetElemAttr("data.postgresql_schemas.regex_test_schema", "schemas.*", "test_schema2"), 56 | resource.TestCheckResourceAttr("data.postgresql_schemas.system_true_not_like_pg", "schemas.#", "9"), 57 | resource.TestCheckResourceAttr("data.postgresql_schemas.system_true_like_pg_regex_pg_catalog", "schemas.#", "1"), 58 | resource.TestCheckTypeSetElemAttr("data.postgresql_schemas.system_true_like_pg_regex_pg_catalog", "schemas.*", "pg_catalog"), 59 | resource.TestCheckResourceAttr("data.postgresql_schemas.system_false_like_test_not_like_test_schema_regex_test_schema", "schemas.#", "2"), 60 | resource.TestCheckTypeSetElemAttr("data.postgresql_schemas.system_false_like_test_not_like_test_schema_regex_test_schema", "schemas.*", "test_schema"), 61 | resource.TestCheckTypeSetElemAttr("data.postgresql_schemas.system_false_like_test_not_like_test_schema_regex_test_schema", "schemas.*", "test_schema2"), 62 | resource.TestCheckResourceAttr("data.postgresql_schemas.system_false_likeany_multi", "schemas.#", "2"), 63 | resource.TestCheckResourceAttr("data.postgresql_schemas.system_true_not_like_multi", "schemas.#", "6"), 64 | resource.TestCheckResourceAttr("data.postgresql_schemas.system_true_likeall_multi_not_like_multi", "schemas.#", "1"), 65 | resource.TestCheckTypeSetElemAttr("data.postgresql_schemas.system_true_likeall_multi_not_like_multi", "schemas.*", "test_schema1"), 66 | resource.TestCheckResourceAttr("data.postgresql_schemas.system_true_likeany_multi_not_like_multi", "schemas.#", "3"), 67 | resource.TestCheckResourceAttr("data.postgresql_schemas.system_true_likeall_multi_not_like_multi_regex", "schemas.#", "1"), 68 | resource.TestCheckTypeSetElemAttr("data.postgresql_schemas.system_true_likeall_multi_not_like_multi_regex", "schemas.*", "test_exp"), 69 | resource.TestCheckResourceAttr("data.postgresql_schemas.system_true_likeany_multi_not_like_multi_regex", "schemas.#", "3"), 70 | ), 71 | }, 72 | }, 73 | }) 74 | } 75 | 76 | func generateDataSourceSchemasConfig(dbName string) string { 77 | return fmt.Sprintf(` 78 | data "postgresql_schemas" "system_false" { 79 | database = "%[1]s" 80 | include_system_schemas = false 81 | } 82 | 83 | data "postgresql_schemas" "no_match" { 84 | database = "%[1]s" 85 | like_any_patterns = ["no_match"] 86 | } 87 | 88 | data "postgresql_schemas" "system_false_like_exp" { 89 | database = "%[1]s" 90 | include_system_schemas = false 91 | like_any_patterns = ["%%exp%%"] 92 | } 93 | 94 | data "postgresql_schemas" "system_true_like_exp" { 95 | database = "%[1]s" 96 | include_system_schemas = true 97 | like_any_patterns = ["%%exp%%"] 98 | } 99 | 100 | data "postgresql_schemas" "like_pg" { 101 | database = "%[1]s" 102 | like_any_patterns = ["pg_%%"] 103 | } 104 | 105 | data "postgresql_schemas" "system_false_like_pg" { 106 | database = "%[1]s" 107 | include_system_schemas = false 108 | like_any_patterns = ["pg_%%"] 109 | } 110 | 111 | data "postgresql_schemas" "system_false_like_pg_double_wildcard" { 112 | database = "%[1]s" 113 | include_system_schemas = false 114 | like_all_patterns = ["%%pg%%"] 115 | } 116 | 117 | data "postgresql_schemas" "system_true_like_information_schema" { 118 | database = "%[1]s" 119 | include_system_schemas = true 120 | like_all_patterns = ["information_schema%%"] 121 | } 122 | 123 | data "postgresql_schemas" "like_test_schema" { 124 | database = "%[1]s" 125 | like_all_patterns = ["test_schema%%"] 126 | } 127 | 128 | data "postgresql_schemas" "regex_test_schema" { 129 | database = "%[1]s" 130 | regex_pattern = "^test_schema.*$" 131 | } 132 | 133 | data "postgresql_schemas" "system_true_not_like_pg" { 134 | database = "%[1]s" 135 | include_system_schemas = true 136 | not_like_all_patterns = ["pg_%%"] 137 | } 138 | 139 | data "postgresql_schemas" "system_true_like_pg_regex_pg_catalog" { 140 | database = "%[1]s" 141 | include_system_schemas = true 142 | like_any_patterns = ["pg_%%"] 143 | regex_pattern = "^pg_catalog.*$" 144 | } 145 | 146 | data "postgresql_schemas" "system_false_like_test_not_like_test_schema_regex_test_schema" { 147 | database = "%[1]s" 148 | include_system_schemas = false 149 | like_any_patterns = ["test_%%"] 150 | not_like_all_patterns = ["test_schema1%%"] 151 | regex_pattern = "^test_schema.*$" 152 | } 153 | 154 | data "postgresql_schemas" "system_false_likeany_multi" { 155 | database = "%[1]s" 156 | include_system_schemas = false 157 | like_any_patterns = ["test_schema1","test_exp"] 158 | } 159 | 160 | data "postgresql_schemas" "system_true_not_like_multi" { 161 | database = "%[1]s" 162 | include_system_schemas = true 163 | not_like_all_patterns = ["%%pg%%","%%exp%%"] 164 | } 165 | 166 | data "postgresql_schemas" "system_true_likeall_multi_not_like_multi" { 167 | database = "%[1]s" 168 | include_system_schemas = true 169 | like_all_patterns = ["%%test%%", "%%1"] 170 | not_like_all_patterns = ["%%pg%%","%%exp%%"] 171 | } 172 | 173 | data "postgresql_schemas" "system_true_likeany_multi_not_like_multi" { 174 | database = "%[1]s" 175 | include_system_schemas = true 176 | like_any_patterns = ["%%test%%", "%%1"] 177 | not_like_all_patterns = ["%%pg%%","%%exp%%"] 178 | } 179 | 180 | data "postgresql_schemas" "system_true_likeall_multi_not_like_multi_regex" { 181 | database = "%[1]s" 182 | include_system_schemas = true 183 | like_all_patterns= ["%%exp%%", "%%test%%"] 184 | not_like_all_patterns = ["%%1%%","%%2%%"] 185 | regex_pattern = "^test_.*$" 186 | } 187 | 188 | data "postgresql_schemas" "system_true_likeany_multi_not_like_multi_regex" { 189 | database = "%[1]s" 190 | include_system_schemas = true 191 | like_any_patterns= ["%%exp%%", "%%test%%"] 192 | not_like_all_patterns = ["%%1%%","%%2%%"] 193 | regex_pattern = "^test_.*$" 194 | } 195 | `, dbName) 196 | } 197 | -------------------------------------------------------------------------------- /postgresql/data_source_postgresql_sequences.go: -------------------------------------------------------------------------------- 1 | package postgresql 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 8 | ) 9 | 10 | const ( 11 | sequenceQuery = ` 12 | SELECT sequence_name, sequence_schema, data_type 13 | FROM information_schema.sequences 14 | ` 15 | sequencePatternMatchingTarget = "sequence_name" 16 | sequenceSchemaKeyword = "sequence_schema" 17 | ) 18 | 19 | func dataSourcePostgreSQLDatabaseSequences() *schema.Resource { 20 | return &schema.Resource{ 21 | Read: PGResourceFunc(dataSourcePostgreSQLSequencesRead), 22 | Schema: map[string]*schema.Schema{ 23 | "database": { 24 | Type: schema.TypeString, 25 | Required: true, 26 | ForceNew: true, 27 | Description: "The PostgreSQL database which will be queried for sequence names", 28 | }, 29 | "schemas": { 30 | Type: schema.TypeList, 31 | Optional: true, 32 | Elem: &schema.Schema{Type: schema.TypeString}, 33 | MinItems: 0, 34 | Description: "The PostgreSQL schema(s) which will be queried for sequence names. Queries all schemas in the database by default", 35 | }, 36 | "like_any_patterns": { 37 | Type: schema.TypeList, 38 | Optional: true, 39 | Elem: &schema.Schema{Type: schema.TypeString}, 40 | MinItems: 0, 41 | Description: "Expression(s) which will be pattern matched against sequence names in the query using the PostgreSQL LIKE ANY operator", 42 | }, 43 | "like_all_patterns": { 44 | Type: schema.TypeList, 45 | Optional: true, 46 | Elem: &schema.Schema{Type: schema.TypeString}, 47 | MinItems: 0, 48 | Description: "Expression(s) which will be pattern matched against sequence names in the query using the PostgreSQL LIKE ALL operator", 49 | }, 50 | "not_like_all_patterns": { 51 | Type: schema.TypeList, 52 | Optional: true, 53 | Elem: &schema.Schema{Type: schema.TypeString}, 54 | MinItems: 0, 55 | Description: "Expression(s) which will be pattern matched against sequence names in the query using the PostgreSQL NOT LIKE ALL operator", 56 | }, 57 | "regex_pattern": { 58 | Type: schema.TypeString, 59 | Optional: true, 60 | Description: "Expression which will be pattern matched against sequence names in the query using the PostgreSQL ~ (regular expression match) operator", 61 | }, 62 | "sequences": { 63 | Type: schema.TypeList, 64 | Computed: true, 65 | Elem: &schema.Resource{ 66 | Schema: map[string]*schema.Schema{ 67 | "object_name": { 68 | Type: schema.TypeString, 69 | Computed: true, 70 | }, 71 | "schema_name": { 72 | Type: schema.TypeString, 73 | Computed: true, 74 | }, 75 | "data_type": { 76 | Type: schema.TypeString, 77 | Computed: true, 78 | }, 79 | }, 80 | }, 81 | Description: "The list of PostgreSQL sequence names retrieved by this data source. Note that this returns a set, so duplicate table names across different schemas will be consolidated.", 82 | }, 83 | }, 84 | } 85 | } 86 | 87 | func dataSourcePostgreSQLSequencesRead(db *DBConnection, d *schema.ResourceData) error { 88 | database := d.Get("database").(string) 89 | 90 | txn, err := startTransaction(db.client, database) 91 | if err != nil { 92 | return err 93 | } 94 | defer deferredRollback(txn) 95 | 96 | query := sequenceQuery 97 | queryConcatKeyword := queryConcatKeywordWhere 98 | 99 | query = applySequenceDataSourceQueryFilters(query, queryConcatKeyword, d) 100 | 101 | rows, err := txn.Query(query) 102 | if err != nil { 103 | return err 104 | } 105 | defer rows.Close() 106 | 107 | sequences := make([]interface{}, 0) 108 | for rows.Next() { 109 | var object_name string 110 | var schema_name string 111 | var data_type string 112 | 113 | if err = rows.Scan(&object_name, &schema_name, &data_type); err != nil { 114 | return fmt.Errorf("could not scan sequence output for database: %w", err) 115 | } 116 | 117 | result := make(map[string]interface{}) 118 | result["object_name"] = object_name 119 | result["schema_name"] = schema_name 120 | result["data_type"] = data_type 121 | sequences = append(sequences, result) 122 | } 123 | 124 | d.Set("sequences", sequences) 125 | d.SetId(generateDataSourceSequencesID(d, database)) 126 | 127 | return nil 128 | } 129 | 130 | func generateDataSourceSequencesID(d *schema.ResourceData, databaseName string) string { 131 | return strings.Join([]string{ 132 | databaseName, 133 | generatePatternArrayString(d.Get("schemas").([]interface{}), queryArrayKeywordAny), 134 | generatePatternArrayString(d.Get("like_any_patterns").([]interface{}), queryArrayKeywordAny), 135 | generatePatternArrayString(d.Get("like_all_patterns").([]interface{}), queryArrayKeywordAll), 136 | generatePatternArrayString(d.Get("not_like_all_patterns").([]interface{}), queryArrayKeywordAll), 137 | d.Get("regex_pattern").(string), 138 | }, "_") 139 | } 140 | 141 | func applySequenceDataSourceQueryFilters(query string, queryConcatKeyword string, d *schema.ResourceData) string { 142 | filters := []string{} 143 | schemasTypeFilter := applyTypeMatchingToQuery(sequenceSchemaKeyword, d.Get("schemas").([]interface{})) 144 | if len(schemasTypeFilter) > 0 { 145 | filters = append(filters, schemasTypeFilter) 146 | } 147 | filters = append(filters, applyPatternMatchingToQuery(sequencePatternMatchingTarget, d)...) 148 | 149 | return finalizeQueryWithFilters(query, queryConcatKeyword, filters) 150 | } 151 | -------------------------------------------------------------------------------- /postgresql/data_source_postgresql_sequences_test.go: -------------------------------------------------------------------------------- 1 | package postgresql 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" 8 | ) 9 | 10 | func TestAccPostgresqlDataSourceSequences(t *testing.T) { 11 | skipIfNotAcc(t) 12 | 13 | // Create the database outside of resource.Test 14 | // because we need to create test schemas. 15 | dbSuffix, teardown := setupTestDatabase(t, true, true) 16 | defer teardown() 17 | 18 | schemas := []string{"test_schema1", "test_schema2"} 19 | createTestSchemas(t, dbSuffix, schemas, "") 20 | 21 | testSequences := []string{"test_schema.test_sequence", "test_schema1.test_sequence1", "test_schema1.test_sequence2", "test_schema2.test_sequence1"} 22 | createTestSequences(t, dbSuffix, testSequences, "") 23 | 24 | dbName, _ := getTestDBNames(dbSuffix) 25 | 26 | testAccPostgresqlDataSourceSequencesDatabaseConfig := generateDataSourceSequencesConfig(dbName) 27 | 28 | resource.Test(t, resource.TestCase{ 29 | PreCheck: func() { testAccPreCheck(t) }, 30 | Providers: testAccProviders, 31 | Steps: []resource.TestStep{ 32 | { 33 | Config: testAccPostgresqlDataSourceSequencesDatabaseConfig, 34 | Check: resource.ComposeTestCheckFunc( 35 | resource.TestCheckResourceAttr("data.postgresql_sequences.test_schema", "sequences.#", "1"), 36 | resource.TestCheckResourceAttr("data.postgresql_sequences.test_schema", "sequences.0.object_name", "test_sequence"), 37 | resource.TestCheckResourceAttr("data.postgresql_sequences.test_schema", "sequences.0.schema_name", "test_schema"), 38 | resource.TestCheckResourceAttr("data.postgresql_sequences.test_schemas1and2", "sequences.#", "3"), 39 | resource.TestCheckResourceAttr("data.postgresql_sequences.test_schemas_like_all_sequence1", "sequences.#", "2"), 40 | resource.TestCheckResourceAttr("data.postgresql_sequences.test_schemas_like_all_sequence1and2", "sequences.#", "0"), 41 | resource.TestCheckResourceAttr("data.postgresql_sequences.test_schemas_like_any_sequence1and2", "sequences.#", "3"), 42 | resource.TestCheckResourceAttr("data.postgresql_sequences.test_schemas_not_like_all_sequence1and2", "sequences.#", "1"), 43 | resource.TestCheckResourceAttr("data.postgresql_sequences.test_schemas_not_like_all_sequence1and2", "sequences.0.object_name", "test_sequence"), 44 | resource.TestCheckResourceAttr("data.postgresql_sequences.test_schemas_regex_sequence1", "sequences.#", "2"), 45 | resource.TestCheckResourceAttr("data.postgresql_sequences.test_schemas_combine_filtering", "sequences.#", "1"), 46 | resource.TestCheckResourceAttr("data.postgresql_sequences.test_schemas_combine_filtering", "sequences.0.object_name", "test_sequence2"), 47 | resource.TestCheckResourceAttr("data.postgresql_sequences.test_schemas_combine_filtering", "sequences.0.schema_name", "test_schema1"), 48 | ), 49 | }, 50 | }, 51 | }) 52 | } 53 | 54 | func generateDataSourceSequencesConfig(dbName string) string { 55 | return fmt.Sprintf(` 56 | data "postgresql_sequences" "test_schemas1and2" { 57 | database = "%[1]s" 58 | schemas = ["test_schema1","test_schema2"] 59 | } 60 | 61 | data "postgresql_sequences" "test_schema" { 62 | database = "%[1]s" 63 | schemas = ["test_schema"] 64 | } 65 | 66 | data "postgresql_sequences" "test_schemas_like_all_sequence1" { 67 | database = "%[1]s" 68 | schemas = ["test_schema","test_schema1","test_schema2"] 69 | like_all_patterns = ["test_sequence1"] 70 | } 71 | 72 | data "postgresql_sequences" "test_schemas_like_all_sequence1and2" { 73 | database = "%[1]s" 74 | schemas = ["test_schema","test_schema1","test_schema2"] 75 | like_all_patterns = ["test_sequence1","test_sequence2"] 76 | } 77 | 78 | data "postgresql_sequences" "test_schemas_like_any_sequence1and2" { 79 | database = "%[1]s" 80 | schemas = ["test_schema","test_schema1","test_schema2"] 81 | like_any_patterns = ["test_sequence1","test_sequence2"] 82 | } 83 | 84 | data "postgresql_sequences" "test_schemas_not_like_all_sequence1and2" { 85 | database = "%[1]s" 86 | schemas = ["test_schema","test_schema1","test_schema2"] 87 | not_like_all_patterns = ["test_sequence1","test_sequence2"] 88 | } 89 | 90 | data "postgresql_sequences" "test_schemas_regex_sequence1" { 91 | database = "%[1]s" 92 | schemas = ["test_schema","test_schema1","test_schema2"] 93 | regex_pattern = "^test_sequence1$" 94 | } 95 | 96 | data "postgresql_sequences" "test_schemas_combine_filtering" { 97 | database = "%[1]s" 98 | schemas = ["test_schema","test_schema1","test_schema2"] 99 | like_any_patterns= ["%%2%%"] 100 | not_like_all_patterns = ["%%1%%"] 101 | regex_pattern = "^test_.*$" 102 | } 103 | 104 | # test_basic's output won't be checked as it can return an indeterminate number of system sequences 105 | data "postgresql_sequences" "test_basic" { 106 | database = "%[1]s" 107 | } 108 | 109 | `, dbName) 110 | } 111 | -------------------------------------------------------------------------------- /postgresql/data_source_postgresql_tables.go: -------------------------------------------------------------------------------- 1 | package postgresql 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 8 | ) 9 | 10 | const ( 11 | tableQuery = ` 12 | SELECT table_name, table_schema, table_type 13 | FROM information_schema.tables 14 | ` 15 | tablePatternMatchingTarget = "table_name" 16 | tableSchemaKeyword = "table_schema" 17 | tableTypeKeyword = "table_type" 18 | ) 19 | 20 | func dataSourcePostgreSQLDatabaseTables() *schema.Resource { 21 | return &schema.Resource{ 22 | Read: PGResourceFunc(dataSourcePostgreSQLTablesRead), 23 | Schema: map[string]*schema.Schema{ 24 | "database": { 25 | Type: schema.TypeString, 26 | Required: true, 27 | ForceNew: true, 28 | Description: "The PostgreSQL database which will be queried for table names", 29 | }, 30 | "schemas": { 31 | Type: schema.TypeList, 32 | Optional: true, 33 | Elem: &schema.Schema{Type: schema.TypeString}, 34 | MinItems: 0, 35 | Description: "The PostgreSQL schema(s) which will be queried for table names. Queries all schemas in the database by default", 36 | }, 37 | "table_types": { 38 | Type: schema.TypeList, 39 | Optional: true, 40 | Elem: &schema.Schema{Type: schema.TypeString}, 41 | MinItems: 0, 42 | Description: "The PostgreSQL table types which will be queried for table names. Includes all table types by default. Use 'BASE TABLE' for normal tables only", 43 | }, 44 | "like_any_patterns": { 45 | Type: schema.TypeList, 46 | Optional: true, 47 | Elem: &schema.Schema{Type: schema.TypeString}, 48 | MinItems: 0, 49 | Description: "Expression(s) which will be pattern matched against table names in the query using the PostgreSQL LIKE ANY operator", 50 | }, 51 | "like_all_patterns": { 52 | Type: schema.TypeList, 53 | Optional: true, 54 | Elem: &schema.Schema{Type: schema.TypeString}, 55 | MinItems: 0, 56 | Description: "Expression(s) which will be pattern matched against table names in the query using the PostgreSQL LIKE ALL operator", 57 | }, 58 | "not_like_all_patterns": { 59 | Type: schema.TypeList, 60 | Optional: true, 61 | Elem: &schema.Schema{Type: schema.TypeString}, 62 | MinItems: 0, 63 | Description: "Expression(s) which will be pattern matched against table names in the query using the PostgreSQL NOT LIKE ALL operator", 64 | }, 65 | "regex_pattern": { 66 | Type: schema.TypeString, 67 | Optional: true, 68 | Description: "Expression which will be pattern matched against table names in the query using the PostgreSQL ~ (regular expression match) operator", 69 | }, 70 | "tables": { 71 | Type: schema.TypeList, 72 | Computed: true, 73 | Elem: &schema.Resource{ 74 | Schema: map[string]*schema.Schema{ 75 | "object_name": { 76 | Type: schema.TypeString, 77 | Computed: true, 78 | }, 79 | "schema_name": { 80 | Type: schema.TypeString, 81 | Computed: true, 82 | }, 83 | "table_type": { 84 | Type: schema.TypeString, 85 | Computed: true, 86 | }, 87 | }, 88 | }, 89 | Description: "The list of PostgreSQL tables retrieved by this data source. Note that this returns a set, so duplicate table names across different schemas will be consolidated.", 90 | }, 91 | }, 92 | } 93 | } 94 | 95 | func dataSourcePostgreSQLTablesRead(db *DBConnection, d *schema.ResourceData) error { 96 | database := d.Get("database").(string) 97 | 98 | txn, err := startTransaction(db.client, database) 99 | if err != nil { 100 | return err 101 | } 102 | defer deferredRollback(txn) 103 | 104 | query := tableQuery 105 | queryConcatKeyword := queryConcatKeywordWhere 106 | 107 | query = applyTableDataSourceQueryFilters(query, queryConcatKeyword, d) 108 | 109 | rows, err := txn.Query(query) 110 | if err != nil { 111 | return err 112 | } 113 | defer rows.Close() 114 | 115 | tables := make([]interface{}, 0) 116 | for rows.Next() { 117 | var object_name string 118 | var schema_name string 119 | var table_type string 120 | 121 | if err = rows.Scan(&object_name, &schema_name, &table_type); err != nil { 122 | return fmt.Errorf("could not scan table output for database: %w", err) 123 | } 124 | 125 | result := make(map[string]interface{}) 126 | result["object_name"] = object_name 127 | result["schema_name"] = schema_name 128 | result["table_type"] = table_type 129 | tables = append(tables, result) 130 | } 131 | 132 | d.Set("tables", tables) 133 | d.SetId(generateDataSourceTablesID(d, database)) 134 | 135 | return nil 136 | } 137 | 138 | func generateDataSourceTablesID(d *schema.ResourceData, databaseName string) string { 139 | return strings.Join([]string{ 140 | databaseName, 141 | generatePatternArrayString(d.Get("schemas").([]interface{}), queryArrayKeywordAny), 142 | generatePatternArrayString(d.Get("table_types").([]interface{}), queryArrayKeywordAny), 143 | generatePatternArrayString(d.Get("like_any_patterns").([]interface{}), queryArrayKeywordAny), 144 | generatePatternArrayString(d.Get("like_all_patterns").([]interface{}), queryArrayKeywordAll), 145 | generatePatternArrayString(d.Get("not_like_all_patterns").([]interface{}), queryArrayKeywordAll), 146 | d.Get("regex_pattern").(string), 147 | }, "_") 148 | } 149 | 150 | func applyTableDataSourceQueryFilters(query string, queryConcatKeyword string, d *schema.ResourceData) string { 151 | filters := []string{} 152 | schemasTypeFilter := applyTypeMatchingToQuery(tableSchemaKeyword, d.Get("schemas").([]interface{})) 153 | if len(schemasTypeFilter) > 0 { 154 | filters = append(filters, schemasTypeFilter) 155 | } 156 | tableTypeFilter := applyTypeMatchingToQuery(tableTypeKeyword, d.Get("table_types").([]interface{})) 157 | if len(tableTypeFilter) > 0 { 158 | filters = append(filters, tableTypeFilter) 159 | } 160 | filters = append(filters, applyPatternMatchingToQuery(tablePatternMatchingTarget, d)...) 161 | 162 | return finalizeQueryWithFilters(query, queryConcatKeyword, filters) 163 | } 164 | -------------------------------------------------------------------------------- /postgresql/data_source_postgresql_tables_test.go: -------------------------------------------------------------------------------- 1 | package postgresql 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" 8 | ) 9 | 10 | func TestAccPostgresqlDataSourceTables(t *testing.T) { 11 | skipIfNotAcc(t) 12 | 13 | // Create the database outside of resource.Test 14 | // because we need to create test schemas. 15 | dbSuffix, teardown := setupTestDatabase(t, true, true) 16 | defer teardown() 17 | 18 | schemas := []string{"test_schema1", "test_schema2"} 19 | createTestSchemas(t, dbSuffix, schemas, "") 20 | 21 | testTables := []string{"test_schema.test_table", "test_schema1.test_table1", "test_schema1.test_table2", "test_schema2.test_table1"} 22 | createTestTables(t, dbSuffix, testTables, "") 23 | 24 | dbName, _ := getTestDBNames(dbSuffix) 25 | 26 | testAccPostgresqlDataSourceTablesDatabaseConfig := generateDataSourceTablesConfig(dbName) 27 | 28 | resource.Test(t, resource.TestCase{ 29 | PreCheck: func() { testAccPreCheck(t) }, 30 | Providers: testAccProviders, 31 | Steps: []resource.TestStep{ 32 | { 33 | Config: testAccPostgresqlDataSourceTablesDatabaseConfig, 34 | Check: resource.ComposeTestCheckFunc( 35 | resource.TestCheckResourceAttr("data.postgresql_tables.test_schema", "tables.#", "1"), 36 | resource.TestCheckResourceAttr("data.postgresql_tables.test_schema", "tables.0.object_name", "test_table"), 37 | resource.TestCheckResourceAttr("data.postgresql_tables.test_schema", "tables.0.schema_name", "test_schema"), 38 | resource.TestCheckResourceAttr("data.postgresql_tables.test_schema", "tables.0.table_type", "BASE TABLE"), 39 | resource.TestCheckResourceAttr("data.postgresql_tables.test_schemas1and2", "tables.#", "3"), 40 | resource.TestCheckResourceAttr("data.postgresql_tables.test_schemas1and2_type_base", "tables.#", "3"), 41 | resource.TestCheckResourceAttr("data.postgresql_tables.test_schemas1and2_type_other", "tables.#", "0"), 42 | resource.TestCheckResourceAttr("data.postgresql_tables.test_schemas1and2_type_base_and_other", "tables.#", "3"), 43 | resource.TestCheckResourceAttr("data.postgresql_tables.test_schemas_like_all_table1", "tables.#", "2"), 44 | resource.TestCheckResourceAttr("data.postgresql_tables.test_schemas_like_all_table1and2", "tables.#", "0"), 45 | resource.TestCheckResourceAttr("data.postgresql_tables.test_schemas_like_any_table1and2", "tables.#", "3"), 46 | resource.TestCheckResourceAttr("data.postgresql_tables.test_schemas_not_like_all_table1and2", "tables.#", "1"), 47 | resource.TestCheckResourceAttr("data.postgresql_tables.test_schemas_not_like_all_table1and2", "tables.0.object_name", "test_table"), 48 | resource.TestCheckResourceAttr("data.postgresql_tables.test_schemas_regex_table1", "tables.#", "2"), 49 | resource.TestCheckResourceAttr("data.postgresql_tables.test_schemas_combine_filtering", "tables.#", "1"), 50 | resource.TestCheckResourceAttr("data.postgresql_tables.test_schemas_combine_filtering", "tables.0.object_name", "test_table2"), 51 | resource.TestCheckResourceAttr("data.postgresql_tables.test_schemas_combine_filtering", "tables.0.schema_name", "test_schema1"), 52 | ), 53 | }, 54 | }, 55 | }) 56 | } 57 | 58 | func generateDataSourceTablesConfig(dbName string) string { 59 | return fmt.Sprintf(` 60 | data "postgresql_tables" "test_schemas1and2" { 61 | database = "%[1]s" 62 | schemas = ["test_schema1","test_schema2"] 63 | } 64 | 65 | data "postgresql_tables" "test_schema" { 66 | database = "%[1]s" 67 | schemas = ["test_schema"] 68 | } 69 | 70 | data "postgresql_tables" "test_schemas1and2_type_base" { 71 | database = "%[1]s" 72 | schemas = ["test_schema1","test_schema2"] 73 | table_types = ["BASE TABLE"] 74 | } 75 | 76 | data "postgresql_tables" "test_schemas1and2_type_other" { 77 | database = "%[1]s" 78 | schemas = ["test_schema1","test_schema2"] 79 | table_types = ["VIEW","LOCAL TEMPORARY"] 80 | } 81 | 82 | data "postgresql_tables" "test_schemas1and2_type_base_and_other" { 83 | database = "%[1]s" 84 | schemas = ["test_schema1","test_schema2"] 85 | table_types = ["VIEW","LOCAL TEMPORARY","BASE TABLE"] 86 | } 87 | 88 | data "postgresql_tables" "test_schemas_like_all_table1" { 89 | database = "%[1]s" 90 | schemas = ["test_schema","test_schema1","test_schema2"] 91 | like_all_patterns = ["test_table1"] 92 | } 93 | 94 | data "postgresql_tables" "test_schemas_like_all_table1and2" { 95 | database = "%[1]s" 96 | schemas = ["test_schema","test_schema1","test_schema2"] 97 | like_all_patterns = ["test_table1","test_table2"] 98 | } 99 | 100 | data "postgresql_tables" "test_schemas_like_any_table1and2" { 101 | database = "%[1]s" 102 | schemas = ["test_schema","test_schema1","test_schema2"] 103 | like_any_patterns = ["test_table1","test_table2"] 104 | } 105 | 106 | data "postgresql_tables" "test_schemas_not_like_all_table1and2" { 107 | database = "%[1]s" 108 | schemas = ["test_schema","test_schema1","test_schema2"] 109 | not_like_all_patterns = ["test_table1","test_table2"] 110 | } 111 | 112 | data "postgresql_tables" "test_schemas_regex_table1" { 113 | database = "%[1]s" 114 | schemas = ["test_schema","test_schema1","test_schema2"] 115 | regex_pattern = "^test_table1$" 116 | } 117 | 118 | data "postgresql_tables" "test_schemas_combine_filtering" { 119 | database = "%[1]s" 120 | schemas = ["test_schema","test_schema1","test_schema2"] 121 | like_any_patterns= ["%%2%%"] 122 | not_like_all_patterns = ["%%1%%"] 123 | regex_pattern = "^test_.*$" 124 | } 125 | 126 | # test_basic's output won't be checked as it can return an indeterminate number of system tables 127 | data "postgresql_tables" "test_basic" { 128 | database = "%[1]s" 129 | } 130 | 131 | `, dbName) 132 | } 133 | -------------------------------------------------------------------------------- /postgresql/helpers_test.go: -------------------------------------------------------------------------------- 1 | package postgresql 2 | 3 | import ( 4 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestFindStringSubmatchMap(t *testing.T) { 11 | 12 | resultMap := findStringSubmatchMap(`(?si).*\$(?P.*)\$.*`, "aa $something_to_extract$ bb") 13 | 14 | assert.Equal(t, 15 | resultMap, 16 | map[string]string{ 17 | "Body": "something_to_extract", 18 | }, 19 | ) 20 | } 21 | 22 | func TestQuoteTableName(t *testing.T) { 23 | tests := []struct { 24 | name string 25 | input string 26 | expected string 27 | }{ 28 | { 29 | name: "simple table name", 30 | input: "users", 31 | expected: `"users"`, 32 | }, 33 | { 34 | name: "table name with schema", 35 | input: "test.users", 36 | expected: `"test"."users"`, 37 | }, 38 | } 39 | 40 | for _, tt := range tests { 41 | t.Run(tt.name, func(t *testing.T) { 42 | actual := quoteTableName(tt.input) 43 | if actual != tt.expected { 44 | t.Errorf("quoteTableName() = %v, want %v", actual, tt.expected) 45 | } 46 | }) 47 | } 48 | } 49 | 50 | func TestArePrivilegesEqual(t *testing.T) { 51 | 52 | type PrivilegesTestObject struct { 53 | d *schema.ResourceData 54 | granted *schema.Set 55 | wanted *schema.Set 56 | assertion bool 57 | } 58 | 59 | tt := []PrivilegesTestObject{ 60 | { 61 | buildResourceData("database", t), 62 | buildPrivilegesSet("CONNECT", "CREATE", "TEMPORARY"), 63 | buildPrivilegesSet("ALL"), 64 | true, 65 | }, 66 | { 67 | buildResourceData("database", t), 68 | buildPrivilegesSet("CREATE", "USAGE"), 69 | buildPrivilegesSet("USAGE"), 70 | false, 71 | }, 72 | { 73 | buildResourceData("table", t), 74 | buildPrivilegesSet("SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE", "REFERENCES", "TRIGGER"), 75 | buildPrivilegesSet("ALL"), 76 | true, 77 | }, 78 | { 79 | buildResourceData("table", t), 80 | buildPrivilegesSet("SELECT"), 81 | buildPrivilegesSet("SELECT, INSERT"), 82 | false, 83 | }, 84 | { 85 | buildResourceData("schema", t), 86 | buildPrivilegesSet("CREATE", "USAGE"), 87 | buildPrivilegesSet("ALL"), 88 | true, 89 | }, 90 | { 91 | buildResourceData("schema", t), 92 | buildPrivilegesSet("CREATE"), 93 | buildPrivilegesSet("ALL"), 94 | false, 95 | }, 96 | } 97 | 98 | for _, configuration := range tt { 99 | err := configuration.d.Set("privileges", configuration.wanted) 100 | assert.NoError(t, err) 101 | equal := resourcePrivilegesEqual(configuration.granted, configuration.d) 102 | assert.Equal(t, configuration.assertion, equal) 103 | } 104 | } 105 | 106 | func buildPrivilegesSet(grants ...interface{}) *schema.Set { 107 | return schema.NewSet(schema.HashString, grants) 108 | } 109 | 110 | func buildResourceData(objectType string, t *testing.T) *schema.ResourceData { 111 | var testSchema = map[string]*schema.Schema{ 112 | "object_type": {Type: schema.TypeString}, 113 | "privileges": { 114 | Type: schema.TypeSet, 115 | Elem: &schema.Schema{Type: schema.TypeString}, 116 | Set: schema.HashString, 117 | }, 118 | } 119 | 120 | m := make(map[string]any) 121 | m["object_type"] = objectType 122 | return schema.TestResourceDataRaw(t, testSchema, m) 123 | } 124 | -------------------------------------------------------------------------------- /postgresql/model_pg_function.go: -------------------------------------------------------------------------------- 1 | package postgresql 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 7 | ) 8 | 9 | // PGFunction is the model for the database function 10 | type PGFunction struct { 11 | Schema string 12 | Name string 13 | Returns string 14 | Language string 15 | Body string 16 | Args []PGFunctionArg 17 | Parallel string 18 | SecurityDefiner bool 19 | Strict bool 20 | Volatility string 21 | } 22 | 23 | type PGFunctionArg struct { 24 | Name string 25 | Type string 26 | Mode string 27 | Default string 28 | } 29 | 30 | func (pgFunction *PGFunction) FromResourceData(d *schema.ResourceData) error { 31 | 32 | if v, ok := d.GetOk(funcSchemaAttr); ok { 33 | pgFunction.Schema = v.(string) 34 | } else { 35 | pgFunction.Schema = "public" 36 | } 37 | 38 | pgFunction.Name = d.Get(funcNameAttr).(string) 39 | pgFunction.Returns = d.Get(funcReturnsAttr).(string) 40 | if v, ok := d.GetOk(funcLanguageAttr); ok { 41 | pgFunction.Language = v.(string) 42 | } else { 43 | pgFunction.Language = "plpgsql" 44 | } 45 | pgFunction.Body = normalizeFunctionBody(d.Get(funcBodyAttr).(string)) 46 | pgFunction.Args = []PGFunctionArg{} 47 | 48 | if v, ok := d.GetOk(funcParallelAttr); ok { 49 | pgFunction.Parallel = v.(string) 50 | } else { 51 | pgFunction.Parallel = defaultFunctionParallel 52 | } 53 | if v, ok := d.GetOk(funcStrictAttr); ok { 54 | pgFunction.Strict = v.(bool) 55 | } else { 56 | pgFunction.Strict = false 57 | } 58 | if v, ok := d.GetOk(funcSecurityDefinerAttr); ok { 59 | pgFunction.SecurityDefiner = v.(bool) 60 | } else { 61 | pgFunction.SecurityDefiner = false 62 | } 63 | if v, ok := d.GetOk(funcVolatilityAttr); ok { 64 | pgFunction.Volatility = v.(string) 65 | } else { 66 | pgFunction.Volatility = defaultFunctionVolatility 67 | } 68 | 69 | // For the main returns if not provided 70 | argOutput := "void" 71 | 72 | if args, ok := d.GetOk(funcArgAttr); ok { 73 | args := args.([]interface{}) 74 | 75 | for _, arg := range args { 76 | arg := arg.(map[string]interface{}) 77 | 78 | var pgArg PGFunctionArg 79 | 80 | if v, ok := arg[funcArgModeAttr]; ok { 81 | pgArg.Mode = v.(string) 82 | } 83 | 84 | if v, ok := arg[funcArgNameAttr]; ok { 85 | pgArg.Name = v.(string) 86 | } 87 | 88 | pgArg.Type = arg[funcArgTypeAttr].(string) 89 | 90 | if v, ok := arg[funcArgDefaultAttr]; ok { 91 | pgArg.Default = v.(string) 92 | } 93 | 94 | // For the main returns if not provided 95 | if strings.ToUpper(pgArg.Mode) == "OUT" { 96 | argOutput = pgArg.Type 97 | } 98 | 99 | pgFunction.Args = append(pgFunction.Args, pgArg) 100 | } 101 | } 102 | 103 | if v, ok := d.GetOk(funcReturnsAttr); ok { 104 | pgFunction.Returns = v.(string) 105 | } else { 106 | pgFunction.Returns = argOutput 107 | } 108 | 109 | return nil 110 | } 111 | 112 | func (pgFunction *PGFunction) Parse(functionDefinition string) error { 113 | 114 | pgFunctionData := findStringSubmatchMap( 115 | `(?si)CREATE\sOR\sREPLACE\sFUNCTION\s(?P[^.]+)\.(?P[^(]+)\((?P.*)\).*RETURNS\s(?P[^\n]+).*LANGUAGE\s(?P[^\n\s]+)\s*(?P(STABLE|IMMUTABLE)?)\s*(?P(PARALLEL (SAFE|RESTRICTED))?)\s*(?P(STRICT)?)\s*(?P(SECURITY DEFINER)?).*\$[a-zA-Z]*\$(?P.*)\$[a-zA-Z]*\$`, 116 | functionDefinition, 117 | ) 118 | 119 | argsData := pgFunctionData["Args"] 120 | 121 | args := []PGFunctionArg{} 122 | 123 | if argsData != "" { 124 | rawArgs := strings.Split(argsData, ",") 125 | for i := 0; i < len(rawArgs); i++ { 126 | var arg PGFunctionArg 127 | err := arg.Parse(rawArgs[i]) 128 | if err != nil { 129 | continue 130 | } 131 | args = append(args, arg) 132 | } 133 | } 134 | 135 | pgFunction.Schema = pgFunctionData["Schema"] 136 | pgFunction.Name = pgFunctionData["Name"] 137 | pgFunction.Returns = pgFunctionData["Returns"] 138 | pgFunction.Language = pgFunctionData["Language"] 139 | pgFunction.Body = pgFunctionData["Body"] 140 | pgFunction.Args = args 141 | pgFunction.SecurityDefiner = len(pgFunctionData["Security"]) > 0 142 | pgFunction.Strict = len(pgFunctionData["Strict"]) > 0 143 | if len(pgFunctionData["Volatility"]) == 0 { 144 | pgFunction.Volatility = defaultFunctionVolatility 145 | } else { 146 | pgFunction.Volatility = pgFunctionData["Volatility"] 147 | } 148 | if len(pgFunctionData["Parallel"]) == 0 { 149 | pgFunction.Parallel = defaultFunctionParallel 150 | } else { 151 | pgFunction.Parallel = strings.TrimPrefix(pgFunctionData["Parallel"], "PARALLEL ") 152 | } 153 | 154 | return nil 155 | } 156 | 157 | func (pgFunctionArg *PGFunctionArg) Parse(functionArgDefinition string) error { 158 | 159 | // Check if default exists 160 | argDefinitions := findStringSubmatchMap(`(?si)(?P.*)\sDEFAULT\s(?P.*)`, functionArgDefinition) 161 | 162 | argData := functionArgDefinition 163 | if len(argDefinitions) > 0 { 164 | argData = argDefinitions["ArgData"] 165 | pgFunctionArg.Default = argDefinitions["ArgDefault"] 166 | } 167 | 168 | pgFunctionArgData := findStringSubmatchMap(`(?si)((?PIN|OUT|INOUT|VARIADIC)\s)?(?P[^\s]+)\s(?P.*)`, argData) 169 | 170 | pgFunctionArg.Name = pgFunctionArgData["Name"] 171 | pgFunctionArg.Type = pgFunctionArgData["Type"] 172 | pgFunctionArg.Mode = pgFunctionArgData["Mode"] 173 | if pgFunctionArg.Mode == "" { 174 | pgFunctionArg.Mode = "IN" 175 | } 176 | return nil 177 | } 178 | 179 | func normalizeFunctionBody(body string) string { 180 | newBodyMap := findStringSubmatchMap(`(?si).*\$[a-zA-Z]*\$\s(?P.*)\s\$[a-zA-Z]*\$.*`, body) 181 | if newBody, ok := newBodyMap["Body"]; ok { 182 | return newBody 183 | } 184 | return body 185 | } 186 | -------------------------------------------------------------------------------- /postgresql/model_pg_function_test.go: -------------------------------------------------------------------------------- 1 | package postgresql 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestFromResourceData(t *testing.T) { 12 | d := mockFunctionResourceData(t, PGFunction{ 13 | Name: "increment", 14 | Body: "BEGIN result = i + 1; END;", 15 | Args: []PGFunctionArg{ 16 | { 17 | Name: "i", 18 | Type: "integer", 19 | }, 20 | { 21 | Name: "result", 22 | Type: "integer", 23 | Mode: "OUT", 24 | }, 25 | }, 26 | }) 27 | 28 | var pgFunction PGFunction 29 | 30 | err := pgFunction.FromResourceData(d) 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | assert.Equal(t, pgFunction, PGFunction{ 35 | Schema: "public", 36 | Name: "increment", 37 | Returns: "integer", 38 | Language: "plpgsql", 39 | Parallel: defaultFunctionParallel, 40 | Strict: false, 41 | SecurityDefiner: false, 42 | Volatility: defaultFunctionVolatility, 43 | Body: "BEGIN result = i + 1; END;", 44 | Args: []PGFunctionArg{ 45 | { 46 | Name: "i", 47 | Type: "integer", 48 | }, 49 | { 50 | Name: "result", 51 | Type: "integer", 52 | Mode: "OUT", 53 | }, 54 | }, 55 | }) 56 | } 57 | 58 | func TestFromResourceDataWithArguments(t *testing.T) { 59 | d := mockFunctionResourceData(t, PGFunction{ 60 | Name: "increment", 61 | Body: "BEGIN result = i + 1; END;", 62 | Args: []PGFunctionArg{ 63 | { 64 | Name: "i", 65 | Type: "integer", 66 | }, 67 | { 68 | Name: "result", 69 | Type: "integer", 70 | Mode: "OUT", 71 | }, 72 | }, 73 | Parallel: "SAFE", 74 | Strict: true, 75 | SecurityDefiner: true, 76 | Volatility: "IMMUTABLE", 77 | }) 78 | 79 | var pgFunction PGFunction 80 | 81 | err := pgFunction.FromResourceData(d) 82 | if err != nil { 83 | t.Fatal(err) 84 | } 85 | assert.Equal(t, pgFunction, PGFunction{ 86 | Schema: "public", 87 | Name: "increment", 88 | Returns: "integer", 89 | Language: "plpgsql", 90 | Parallel: "SAFE", 91 | Strict: true, 92 | SecurityDefiner: true, 93 | Volatility: "IMMUTABLE", 94 | Body: "BEGIN result = i + 1; END;", 95 | Args: []PGFunctionArg{ 96 | { 97 | Name: "i", 98 | Type: "integer", 99 | }, 100 | { 101 | Name: "result", 102 | Type: "integer", 103 | Mode: "OUT", 104 | }, 105 | }, 106 | }) 107 | } 108 | 109 | func TestPGFunctionParseWithArguments(t *testing.T) { 110 | 111 | var functionDefinition = ` 112 | CREATE OR REPLACE FUNCTION public.pg_func_test(showtext boolean, OUT userid oid, default_null integer DEFAULT NULL::integer, simple_default integer DEFAULT 42, long_default character varying DEFAULT 'foo'::character varying) 113 | RETURNS SETOF record 114 | LANGUAGE c 115 | STABLE PARALLEL SAFE STRICT SECURITY DEFINER 116 | AS $function$pg_func_test_body$function$ 117 | ` 118 | 119 | var pgFunction PGFunction 120 | 121 | err := pgFunction.Parse(functionDefinition) 122 | if err != nil { 123 | t.Fatal(err) 124 | } 125 | 126 | assert.Equal(t, pgFunction, PGFunction{ 127 | Name: "pg_func_test", 128 | Schema: "public", 129 | Returns: "SETOF record", 130 | Language: "c", 131 | Parallel: "SAFE", 132 | SecurityDefiner: true, 133 | Strict: true, 134 | Volatility: "STABLE", 135 | Body: "pg_func_test_body", 136 | Args: []PGFunctionArg{ 137 | { 138 | Mode: "IN", 139 | Name: "showtext", 140 | Type: "boolean", 141 | }, 142 | { 143 | Mode: "OUT", 144 | Name: "userid", 145 | Type: "oid", 146 | }, 147 | { 148 | Mode: "IN", 149 | Name: "default_null", 150 | Type: "integer", 151 | Default: "NULL::integer", 152 | }, 153 | { 154 | Mode: "IN", 155 | Name: "simple_default", 156 | Type: "integer", 157 | Default: "42", 158 | }, 159 | { 160 | Mode: "IN", 161 | Name: "long_default", 162 | Type: "character varying", 163 | Default: "'foo'::character varying", 164 | }, 165 | }, 166 | }) 167 | } 168 | 169 | func TestPGFunctionParseWithoutArguments(t *testing.T) { 170 | 171 | var functionDefinition = ` 172 | CREATE OR REPLACE FUNCTION public.pg_func_test() 173 | RETURNS SETOF record 174 | LANGUAGE plpgsql 175 | AS $function$ 176 | MultiLine Function 177 | $function$ 178 | ` 179 | 180 | var pgFunction PGFunction 181 | 182 | err := pgFunction.Parse(functionDefinition) 183 | if err != nil { 184 | t.Fatal(err) 185 | } 186 | 187 | assert.Equal(t, pgFunction, PGFunction{ 188 | Name: "pg_func_test", 189 | Schema: "public", 190 | Returns: "SETOF record", 191 | Language: "plpgsql", 192 | Parallel: "UNSAFE", 193 | SecurityDefiner: false, 194 | Strict: false, 195 | Volatility: "VOLATILE", 196 | Body: ` 197 | MultiLine Function 198 | `, 199 | Args: []PGFunctionArg{}, 200 | }) 201 | } 202 | 203 | func TestPGFunctionArgParseWithDefault(t *testing.T) { 204 | 205 | var functionArgDefinition = `default_null integer DEFAULT NULL::integer` 206 | 207 | var pgFunctionArg PGFunctionArg 208 | 209 | err := pgFunctionArg.Parse(functionArgDefinition) 210 | if err != nil { 211 | t.Fatal(err) 212 | } 213 | 214 | assert.Equal(t, pgFunctionArg, PGFunctionArg{ 215 | Mode: "IN", 216 | Name: "default_null", 217 | Type: "integer", 218 | Default: "NULL::integer", 219 | }) 220 | } 221 | 222 | func TestPGFunctionArgParseWithoutDefault(t *testing.T) { 223 | 224 | var functionArgDefinition = `num integer` 225 | 226 | var pgFunctionArg PGFunctionArg 227 | 228 | err := pgFunctionArg.Parse(functionArgDefinition) 229 | if err != nil { 230 | t.Fatal(err) 231 | } 232 | 233 | assert.Equal(t, pgFunctionArg, PGFunctionArg{ 234 | Mode: "IN", 235 | Name: "num", 236 | Type: "integer", 237 | }) 238 | } 239 | 240 | func mockFunctionResourceData(t *testing.T, obj PGFunction) *schema.ResourceData { 241 | 242 | state := terraform.InstanceState{} 243 | 244 | state.ID = "" 245 | // Build the attribute map from ForemanModel 246 | attributes := make(map[string]interface{}) 247 | 248 | attributes["name"] = obj.Name 249 | attributes["returns"] = obj.Returns 250 | attributes["language"] = obj.Language 251 | attributes["body"] = obj.Body 252 | attributes["strict"] = obj.Strict 253 | attributes["security_definer"] = obj.SecurityDefiner 254 | attributes["parallel"] = obj.Parallel 255 | attributes["volatility"] = obj.Volatility 256 | 257 | var args []interface{} 258 | 259 | for _, a := range obj.Args { 260 | args = append(args, map[string]interface{}{ 261 | "type": a.Type, 262 | "name": a.Name, 263 | "mode": a.Mode, 264 | "default": a.Default, 265 | }) 266 | } 267 | 268 | attributes["arg"] = args 269 | 270 | return schema.TestResourceDataRaw(t, resourcePostgreSQLFunction().Schema, attributes) 271 | } 272 | -------------------------------------------------------------------------------- /postgresql/provider_test.go: -------------------------------------------------------------------------------- 1 | package postgresql 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "testing" 7 | 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" 10 | ) 11 | 12 | var testAccProviders map[string]*schema.Provider 13 | var testAccProvider *schema.Provider 14 | 15 | func init() { 16 | testAccProvider = Provider() 17 | testAccProviders = map[string]*schema.Provider{ 18 | "postgresql": testAccProvider, 19 | } 20 | } 21 | 22 | func TestProvider(t *testing.T) { 23 | if err := Provider().InternalValidate(); err != nil { 24 | t.Fatalf("err: %s", err) 25 | } 26 | } 27 | 28 | func TestProvider_impl(t *testing.T) { 29 | var _ *schema.Provider = Provider() 30 | } 31 | 32 | func testAccPreCheck(t *testing.T) { 33 | var host string 34 | if host = os.Getenv("PGHOST"); host == "" { 35 | t.Fatal("PGHOST must be set for acceptance tests") 36 | } 37 | if v := os.Getenv("PGUSER"); v == "" { 38 | t.Fatal("PGUSER must be set for acceptance tests") 39 | } 40 | 41 | err := testAccProvider.Configure(context.Background(), terraform.NewResourceConfigRaw(nil)) 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /postgresql/proxy_driver.go: -------------------------------------------------------------------------------- 1 | package postgresql 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "database/sql/driver" 7 | "net" 8 | "time" 9 | 10 | "github.com/lib/pq" 11 | "golang.org/x/net/proxy" 12 | ) 13 | 14 | const proxyDriverName = "postgresql-proxy" 15 | 16 | type proxyDriver struct{} 17 | 18 | func (d proxyDriver) Open(name string) (driver.Conn, error) { 19 | return pq.DialOpen(d, name) 20 | } 21 | 22 | func (d proxyDriver) Dial(network, address string) (net.Conn, error) { 23 | dialer := proxy.FromEnvironment() 24 | return dialer.Dial(network, address) 25 | } 26 | 27 | func (d proxyDriver) DialTimeout(network, address string, timeout time.Duration) (net.Conn, error) { 28 | ctx, cancel := context.WithTimeout(context.TODO(), timeout) 29 | defer cancel() 30 | return proxy.Dial(ctx, network, address) 31 | } 32 | 33 | func init() { 34 | sql.Register(proxyDriverName, proxyDriver{}) 35 | } 36 | -------------------------------------------------------------------------------- /postgresql/resource_postgresql_function_test.go: -------------------------------------------------------------------------------- 1 | package postgresql 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" 10 | ) 11 | 12 | func TestAccPostgresqlFunction_Basic(t *testing.T) { 13 | config := ` 14 | resource "postgresql_function" "basic_function" { 15 | name = "basic_function" 16 | returns = "integer" 17 | language = "plpgsql" 18 | body = <<-EOF 19 | BEGIN 20 | RETURN 1; 21 | END; 22 | EOF 23 | } 24 | ` 25 | 26 | resource.Test(t, resource.TestCase{ 27 | PreCheck: func() { 28 | testAccPreCheck(t) 29 | testCheckCompatibleVersion(t, featureFunction) 30 | }, 31 | Providers: testAccProviders, 32 | CheckDestroy: testAccCheckPostgresqlFunctionDestroy, 33 | Steps: []resource.TestStep{ 34 | { 35 | Config: config, 36 | Check: resource.ComposeTestCheckFunc( 37 | testAccCheckPostgresqlFunctionExists("postgresql_function.basic_function", ""), 38 | resource.TestCheckResourceAttr( 39 | "postgresql_function.basic_function", "name", "basic_function"), 40 | resource.TestCheckResourceAttr( 41 | "postgresql_function.basic_function", "schema", "public"), 42 | resource.TestCheckResourceAttr( 43 | "postgresql_function.basic_function", "language", "plpgsql"), 44 | ), 45 | }, 46 | }, 47 | }) 48 | } 49 | 50 | func TestAccPostgresqlFunction_SpecificDatabase(t *testing.T) { 51 | skipIfNotAcc(t) 52 | 53 | dbSuffix, teardown := setupTestDatabase(t, true, true) 54 | defer teardown() 55 | 56 | dbName, _ := getTestDBNames(dbSuffix) 57 | 58 | config := ` 59 | resource "postgresql_function" "basic_function" { 60 | name = "basic_function" 61 | database = "%s" 62 | returns = "integer" 63 | language = "plpgsql" 64 | body = <<-EOF 65 | BEGIN 66 | RETURN 1; 67 | END; 68 | EOF 69 | } 70 | ` 71 | 72 | resource.Test(t, resource.TestCase{ 73 | PreCheck: func() { 74 | testAccPreCheck(t) 75 | testCheckCompatibleVersion(t, featureFunction) 76 | }, 77 | Providers: testAccProviders, 78 | CheckDestroy: testAccCheckPostgresqlFunctionDestroy, 79 | Steps: []resource.TestStep{ 80 | { 81 | Config: fmt.Sprintf(config, dbName), 82 | Check: resource.ComposeTestCheckFunc( 83 | testAccCheckPostgresqlFunctionExists("postgresql_function.basic_function", dbName), 84 | resource.TestCheckResourceAttr( 85 | "postgresql_function.basic_function", "name", "basic_function"), 86 | resource.TestCheckResourceAttr( 87 | "postgresql_function.basic_function", "database", dbName), 88 | resource.TestCheckResourceAttr( 89 | "postgresql_function.basic_function", "schema", "public"), 90 | resource.TestCheckResourceAttr( 91 | "postgresql_function.basic_function", "language", "plpgsql"), 92 | ), 93 | }, 94 | }, 95 | }) 96 | } 97 | 98 | func TestAccPostgresqlFunction_MultipleArgs(t *testing.T) { 99 | config := ` 100 | resource "postgresql_schema" "test" { 101 | name = "test" 102 | } 103 | 104 | resource "postgresql_function" "increment" { 105 | schema = postgresql_schema.test.name 106 | name = "increment" 107 | arg { 108 | name = "i" 109 | type = "integer" 110 | default = "7" 111 | } 112 | arg { 113 | name = "result" 114 | type = "integer" 115 | mode = "OUT" 116 | } 117 | language = "plpgsql" 118 | parallel = "RESTRICTED" 119 | strict = true 120 | security_definer = true 121 | volatility = "STABLE" 122 | body = <<-EOF 123 | BEGIN 124 | result = i + 1; 125 | END; 126 | EOF 127 | } 128 | ` 129 | 130 | resource.Test(t, resource.TestCase{ 131 | PreCheck: func() { 132 | testAccPreCheck(t) 133 | testCheckCompatibleVersion(t, featureFunction) 134 | }, 135 | Providers: testAccProviders, 136 | CheckDestroy: testAccCheckPostgresqlFunctionDestroy, 137 | Steps: []resource.TestStep{ 138 | { 139 | Config: config, 140 | Check: resource.ComposeTestCheckFunc( 141 | testAccCheckPostgresqlFunctionExists("postgresql_function.increment", ""), 142 | resource.TestCheckResourceAttr( 143 | "postgresql_function.increment", "name", "increment"), 144 | resource.TestCheckResourceAttr( 145 | "postgresql_function.increment", "schema", "test"), 146 | resource.TestCheckResourceAttr( 147 | "postgresql_function.increment", "language", "plpgsql"), 148 | resource.TestCheckResourceAttr( 149 | "postgresql_function.increment", "strict", "true"), 150 | resource.TestCheckResourceAttr( 151 | "postgresql_function.increment", "parallel", "RESTRICTED"), 152 | resource.TestCheckResourceAttr( 153 | "postgresql_function.increment", "security_definer", "true"), 154 | resource.TestCheckResourceAttr( 155 | "postgresql_function.increment", "volatility", "STABLE"), 156 | ), 157 | }, 158 | }, 159 | }) 160 | } 161 | 162 | func TestAccPostgresqlFunction_Update(t *testing.T) { 163 | configCreate := ` 164 | resource "postgresql_function" "func" { 165 | name = "func" 166 | returns = "integer" 167 | language = "plpgsql" 168 | body = <<-EOF 169 | BEGIN 170 | RETURN 1; 171 | END; 172 | EOF 173 | } 174 | ` 175 | 176 | configUpdate := ` 177 | resource "postgresql_function" "func" { 178 | name = "func" 179 | returns = "integer" 180 | language = "plpgsql" 181 | volatility = "IMMUTABLE" 182 | body = <<-EOF 183 | BEGIN 184 | RETURN 2; 185 | END; 186 | EOF 187 | } 188 | ` 189 | resource.Test(t, resource.TestCase{ 190 | PreCheck: func() { 191 | testAccPreCheck(t) 192 | testCheckCompatibleVersion(t, featureFunction) 193 | }, 194 | Providers: testAccProviders, 195 | CheckDestroy: testAccCheckPostgresqlFunctionDestroy, 196 | Steps: []resource.TestStep{ 197 | { 198 | Config: configCreate, 199 | Check: resource.ComposeTestCheckFunc( 200 | testAccCheckPostgresqlFunctionExists("postgresql_function.func", ""), 201 | resource.TestCheckResourceAttr( 202 | "postgresql_function.func", "name", "func"), 203 | resource.TestCheckResourceAttr( 204 | "postgresql_function.func", "schema", "public"), 205 | resource.TestCheckResourceAttr( 206 | "postgresql_function.func", "volatility", "VOLATILE"), 207 | ), 208 | }, 209 | { 210 | Config: configUpdate, 211 | Check: resource.ComposeTestCheckFunc( 212 | testAccCheckPostgresqlFunctionExists("postgresql_function.func", ""), 213 | resource.TestCheckResourceAttr( 214 | "postgresql_function.func", "name", "func"), 215 | resource.TestCheckResourceAttr( 216 | "postgresql_function.func", "schema", "public"), 217 | resource.TestCheckResourceAttr( 218 | "postgresql_function.func", "volatility", "IMMUTABLE"), 219 | ), 220 | }, 221 | }, 222 | }) 223 | } 224 | 225 | func testAccCheckPostgresqlFunctionExists(n string, database string) resource.TestCheckFunc { 226 | return func(s *terraform.State) error { 227 | rs, ok := s.RootModule().Resources[n] 228 | if !ok { 229 | return fmt.Errorf("Resource not found: %s", n) 230 | } 231 | 232 | if rs.Primary.ID == "" { 233 | return fmt.Errorf("No ID is set") 234 | } 235 | 236 | signature := rs.Primary.ID 237 | 238 | client := testAccProvider.Meta().(*Client) 239 | txn, err := startTransaction(client, database) 240 | if err != nil { 241 | return err 242 | } 243 | defer deferredRollback(txn) 244 | 245 | exists, err := checkFunctionExists(txn, signature) 246 | 247 | if err != nil { 248 | return fmt.Errorf("Error checking function %s", err) 249 | } 250 | 251 | if !exists { 252 | return fmt.Errorf("Function not found") 253 | } 254 | 255 | return nil 256 | } 257 | } 258 | 259 | func testAccCheckPostgresqlFunctionDestroy(s *terraform.State) error { 260 | client := testAccProvider.Meta().(*Client) 261 | 262 | for _, rs := range s.RootModule().Resources { 263 | if rs.Type != "postgresql_function" { 264 | continue 265 | } 266 | 267 | txn, err := startTransaction(client, "") 268 | if err != nil { 269 | return err 270 | } 271 | defer deferredRollback(txn) 272 | 273 | _, functionSignature, expandErr := expandFunctionID(rs.Primary.ID, nil, nil) 274 | 275 | if expandErr != nil { 276 | return fmt.Errorf("Incorrect resource Id %s", err) 277 | } 278 | 279 | exists, err := checkFunctionExists(txn, functionSignature) 280 | 281 | if err != nil { 282 | return fmt.Errorf("Error checking function %s", err) 283 | } 284 | 285 | if exists { 286 | return fmt.Errorf("Function still exists after destroy") 287 | } 288 | } 289 | 290 | return nil 291 | } 292 | 293 | func checkFunctionExists(txn *sql.Tx, signature string) (bool, error) { 294 | var _rez bool 295 | err := txn.QueryRow(fmt.Sprintf("SELECT to_regprocedure('%s') IS NOT NULL", signature)).Scan(&_rez) 296 | switch { 297 | case err == sql.ErrNoRows: 298 | return false, nil 299 | case err != nil: 300 | return false, fmt.Errorf("Error reading info about function: %s", err) 301 | } 302 | 303 | return _rez, nil 304 | } 305 | -------------------------------------------------------------------------------- /postgresql/resource_postgresql_grant_role.go: -------------------------------------------------------------------------------- 1 | package postgresql 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "log" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 11 | "github.com/lib/pq" 12 | ) 13 | 14 | const ( 15 | // This returns the role membership for role, grant_role 16 | getGrantRoleQuery = ` 17 | SELECT 18 | pg_get_userbyid(member) as role, 19 | pg_get_userbyid(roleid) as grant_role, 20 | admin_option 21 | FROM 22 | pg_auth_members 23 | WHERE 24 | pg_get_userbyid(member) = $1 AND 25 | pg_get_userbyid(roleid) = $2; 26 | ` 27 | ) 28 | 29 | func resourcePostgreSQLGrantRole() *schema.Resource { 30 | return &schema.Resource{ 31 | Create: PGResourceFunc(resourcePostgreSQLGrantRoleCreate), 32 | Read: PGResourceFunc(resourcePostgreSQLGrantRoleRead), 33 | Delete: PGResourceFunc(resourcePostgreSQLGrantRoleDelete), 34 | 35 | Schema: map[string]*schema.Schema{ 36 | "role": { 37 | Type: schema.TypeString, 38 | Required: true, 39 | ForceNew: true, 40 | Description: "The name of the role to grant grant_role", 41 | }, 42 | "grant_role": { 43 | Type: schema.TypeString, 44 | Required: true, 45 | ForceNew: true, 46 | Description: "The name of the role that is granted to role", 47 | }, 48 | "with_admin_option": { 49 | Type: schema.TypeBool, 50 | Optional: true, 51 | ForceNew: true, 52 | Default: false, 53 | Description: "Permit the grant recipient to grant it to others", 54 | }, 55 | }, 56 | } 57 | } 58 | 59 | func resourcePostgreSQLGrantRoleRead(db *DBConnection, d *schema.ResourceData) error { 60 | if !db.featureSupported(featurePrivileges) { 61 | return fmt.Errorf( 62 | "postgresql_grant_role resource is not supported for this Postgres version (%s)", 63 | db.version, 64 | ) 65 | } 66 | 67 | return readGrantRole(db, d) 68 | } 69 | 70 | func resourcePostgreSQLGrantRoleCreate(db *DBConnection, d *schema.ResourceData) error { 71 | if !db.featureSupported(featurePrivileges) { 72 | return fmt.Errorf( 73 | "postgresql_grant_role resource is not supported for this Postgres version (%s)", 74 | db.version, 75 | ) 76 | } 77 | 78 | txn, err := startTransaction(db.client, "") 79 | if err != nil { 80 | return err 81 | } 82 | defer deferredRollback(txn) 83 | 84 | // Revoke the granted roles before granting them again. 85 | if err = revokeRole(txn, d); err != nil { 86 | return err 87 | } 88 | 89 | if err = grantRole(txn, d); err != nil { 90 | return err 91 | } 92 | 93 | if err = txn.Commit(); err != nil { 94 | return fmt.Errorf("could not commit transaction: %w", err) 95 | } 96 | 97 | d.SetId(generateGrantRoleID(d)) 98 | 99 | return readGrantRole(db, d) 100 | } 101 | 102 | func resourcePostgreSQLGrantRoleDelete(db *DBConnection, d *schema.ResourceData) error { 103 | if !db.featureSupported(featurePrivileges) { 104 | return fmt.Errorf( 105 | "postgresql_grant_role resource is not supported for this Postgres version (%s)", 106 | db.version, 107 | ) 108 | } 109 | 110 | txn, err := startTransaction(db.client, "") 111 | if err != nil { 112 | return err 113 | } 114 | defer deferredRollback(txn) 115 | 116 | if err = revokeRole(txn, d); err != nil { 117 | return err 118 | } 119 | 120 | if err = txn.Commit(); err != nil { 121 | return fmt.Errorf("could not commit transaction: %w", err) 122 | } 123 | 124 | return nil 125 | } 126 | 127 | func readGrantRole(db QueryAble, d *schema.ResourceData) error { 128 | var roleName, grantRoleName string 129 | var withAdminOption bool 130 | 131 | grantRoleID := d.Id() 132 | 133 | values := []interface{}{ 134 | &roleName, 135 | &grantRoleName, 136 | &withAdminOption, 137 | } 138 | 139 | err := db.QueryRow(getGrantRoleQuery, d.Get("role"), d.Get("grant_role")).Scan(values...) 140 | switch { 141 | case err == sql.ErrNoRows: 142 | log.Printf("[WARN] PostgreSQL grant role (%q) not found", grantRoleID) 143 | d.SetId("") 144 | return nil 145 | case err != nil: 146 | return fmt.Errorf("Error reading grant role: %w", err) 147 | } 148 | 149 | d.Set("role", roleName) 150 | d.Set("grant_role", grantRoleName) 151 | d.Set("with_admin_option", withAdminOption) 152 | 153 | d.SetId(generateGrantRoleID(d)) 154 | 155 | return nil 156 | } 157 | 158 | func createGrantRoleQuery(d *schema.ResourceData) string { 159 | grantRole, _ := d.Get("grant_role").(string) 160 | role, _ := d.Get("role").(string) 161 | 162 | query := fmt.Sprintf( 163 | "GRANT %s TO %s", 164 | pq.QuoteIdentifier(grantRole), 165 | pq.QuoteIdentifier(role), 166 | ) 167 | if wao, _ := d.Get("with_admin_option").(bool); wao { 168 | query = query + " WITH ADMIN OPTION" 169 | } 170 | 171 | return query 172 | } 173 | 174 | func createRevokeRoleQuery(d *schema.ResourceData) string { 175 | grantRole, _ := d.Get("grant_role").(string) 176 | role, _ := d.Get("role").(string) 177 | 178 | return fmt.Sprintf( 179 | "REVOKE %s FROM %s", 180 | pq.QuoteIdentifier(grantRole), 181 | pq.QuoteIdentifier(role), 182 | ) 183 | } 184 | 185 | func grantRole(txn *sql.Tx, d *schema.ResourceData) error { 186 | query := createGrantRoleQuery(d) 187 | if _, err := txn.Exec(query); err != nil { 188 | return fmt.Errorf("could not execute grant query: %w", err) 189 | } 190 | return nil 191 | } 192 | 193 | func revokeRole(txn *sql.Tx, d *schema.ResourceData) error { 194 | query := createRevokeRoleQuery(d) 195 | if _, err := txn.Exec(query); err != nil { 196 | return fmt.Errorf("could not execute revoke query: %w", err) 197 | } 198 | return nil 199 | } 200 | 201 | func generateGrantRoleID(d *schema.ResourceData) string { 202 | return strings.Join([]string{d.Get("role").(string), d.Get("grant_role").(string), strconv.FormatBool(d.Get("with_admin_option").(bool))}, "_") 203 | } 204 | -------------------------------------------------------------------------------- /postgresql/resource_postgresql_grant_role_test.go: -------------------------------------------------------------------------------- 1 | package postgresql 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "strconv" 7 | "testing" 8 | 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" 10 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 11 | "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" 12 | "github.com/lib/pq" 13 | ) 14 | 15 | func TestCreateGrantRoleQuery(t *testing.T) { 16 | var roleName = "foo" 17 | var grantRoleName = "bar" 18 | 19 | cases := []struct { 20 | resource map[string]interface{} 21 | expected string 22 | }{ 23 | { 24 | resource: map[string]interface{}{ 25 | "role": roleName, 26 | "grant_role": grantRoleName, 27 | }, 28 | expected: fmt.Sprintf("GRANT %s TO %s", pq.QuoteIdentifier(grantRoleName), pq.QuoteIdentifier(roleName)), 29 | }, 30 | { 31 | resource: map[string]interface{}{ 32 | "role": roleName, 33 | "grant_role": grantRoleName, 34 | "with_admin_option": false, 35 | }, 36 | expected: fmt.Sprintf("GRANT %s TO %s", pq.QuoteIdentifier(grantRoleName), pq.QuoteIdentifier(roleName)), 37 | }, 38 | { 39 | resource: map[string]interface{}{ 40 | "role": roleName, 41 | "grant_role": grantRoleName, 42 | "with_admin_option": true, 43 | }, 44 | expected: fmt.Sprintf("GRANT %s TO %s WITH ADMIN OPTION", pq.QuoteIdentifier(grantRoleName), pq.QuoteIdentifier(roleName)), 45 | }, 46 | } 47 | 48 | for _, c := range cases { 49 | out := createGrantRoleQuery(schema.TestResourceDataRaw(t, resourcePostgreSQLGrantRole().Schema, c.resource)) 50 | if out != c.expected { 51 | t.Fatalf("Error matching output and expected: %#v vs %#v", out, c.expected) 52 | } 53 | } 54 | } 55 | 56 | func TestRevokeRoleQuery(t *testing.T) { 57 | var roleName = "foo" 58 | var grantRoleName = "bar" 59 | 60 | expected := fmt.Sprintf("REVOKE %s FROM %s", pq.QuoteIdentifier(grantRoleName), pq.QuoteIdentifier(roleName)) 61 | 62 | cases := []struct { 63 | resource map[string]interface{} 64 | }{ 65 | { 66 | resource: map[string]interface{}{ 67 | "role": roleName, 68 | "grant_role": grantRoleName, 69 | }, 70 | }, 71 | { 72 | resource: map[string]interface{}{ 73 | "role": roleName, 74 | "grant_role": grantRoleName, 75 | "with_admin_option": false, 76 | }, 77 | }, 78 | { 79 | resource: map[string]interface{}{ 80 | "role": roleName, 81 | "grant_role": grantRoleName, 82 | "with_admin_option": true, 83 | }, 84 | }, 85 | } 86 | 87 | for _, c := range cases { 88 | out := createRevokeRoleQuery(schema.TestResourceDataRaw(t, resourcePostgreSQLGrantRole().Schema, c.resource)) 89 | if out != expected { 90 | t.Fatalf("Error matching output and expected: %#v vs %#v", out, expected) 91 | } 92 | } 93 | } 94 | 95 | func TestAccPostgresqlGrantRole(t *testing.T) { 96 | skipIfNotAcc(t) 97 | 98 | config := getTestConfig(t) 99 | dsn := config.connStr("postgres") 100 | 101 | dbSuffix, teardown := setupTestDatabase(t, false, true) 102 | defer teardown() 103 | 104 | _, roleName := getTestDBNames(dbSuffix) 105 | 106 | grantedRoleName := "foo" 107 | 108 | testAccPostgresqlGrantRoleResources := fmt.Sprintf(` 109 | resource postgresql_role "grant" { 110 | name = "%s" 111 | } 112 | resource postgresql_grant_role "grant_role" { 113 | role = "%s" 114 | grant_role = postgresql_role.grant.name 115 | with_admin_option = true 116 | } 117 | `, grantedRoleName, roleName) 118 | 119 | resource.Test(t, resource.TestCase{ 120 | PreCheck: func() { 121 | testAccPreCheck(t) 122 | testCheckCompatibleVersion(t, featurePrivileges) 123 | }, 124 | Providers: testAccProviders, 125 | Steps: []resource.TestStep{ 126 | { 127 | Config: testAccPostgresqlGrantRoleResources, 128 | Check: resource.ComposeTestCheckFunc( 129 | resource.TestCheckResourceAttr( 130 | "postgresql_grant_role.grant_role", "role", roleName), 131 | resource.TestCheckResourceAttr( 132 | "postgresql_grant_role.grant_role", "grant_role", grantedRoleName), 133 | resource.TestCheckResourceAttr( 134 | "postgresql_grant_role.grant_role", "with_admin_option", strconv.FormatBool(true)), 135 | checkGrantRole(t, dsn, roleName, grantedRoleName, true), 136 | ), 137 | }, 138 | }, 139 | }) 140 | } 141 | 142 | func checkGrantRole(t *testing.T, dsn, role string, grantRole string, withAdmin bool) resource.TestCheckFunc { 143 | return func(s *terraform.State) error { 144 | db, err := sql.Open("postgres", dsn) 145 | if err != nil { 146 | t.Fatalf("could to create connection pool: %v", err) 147 | } 148 | defer db.Close() 149 | 150 | var _rez int 151 | err = db.QueryRow(` 152 | SELECT 1 153 | FROM pg_auth_members 154 | WHERE pg_get_userbyid(member) = $1 155 | AND pg_get_userbyid(roleid) = $2 156 | AND admin_option = $3; 157 | `, role, grantRole, withAdmin).Scan(&_rez) 158 | 159 | switch { 160 | case err == sql.ErrNoRows: 161 | return fmt.Errorf( 162 | "Role %s is not a member of %s", 163 | role, grantRole, 164 | ) 165 | 166 | case err != nil: 167 | t.Fatalf("could not check granted role: %v", err) 168 | } 169 | 170 | return nil 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /postgresql/resource_postgresql_physical_replication_slot.go: -------------------------------------------------------------------------------- 1 | package postgresql 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 7 | ) 8 | 9 | func resourcePostgreSQLPhysicalReplicationSlot() *schema.Resource { 10 | return &schema.Resource{ 11 | Create: PGResourceFunc(resourcePostgreSQLPhysicalReplicationSlotCreate), 12 | Read: PGResourceFunc(resourcePostgreSQLPhysicalReplicationSlotRead), 13 | Delete: PGResourceFunc(resourcePostgreSQLPhysicalReplicationSlotDelete), 14 | Exists: PGResourceExistsFunc(resourcePostgreSQLPhysicalReplicationSlotExists), 15 | Importer: &schema.ResourceImporter{ 16 | StateContext: schema.ImportStatePassthroughContext, 17 | }, 18 | 19 | Schema: map[string]*schema.Schema{ 20 | "name": { 21 | Type: schema.TypeString, 22 | Required: true, 23 | ForceNew: true, 24 | }, 25 | }, 26 | } 27 | } 28 | 29 | func resourcePostgreSQLPhysicalReplicationSlotCreate(db *DBConnection, d *schema.ResourceData) error { 30 | name := d.Get("name").(string) 31 | sql := "SELECT FROM pg_create_physical_replication_slot($1)" 32 | if _, err := db.Exec(sql, name); err != nil { 33 | return fmt.Errorf("could not create physical ReplicationSlot %s: %w", name, err) 34 | } 35 | d.SetId(name) 36 | 37 | return nil 38 | } 39 | 40 | func resourcePostgreSQLPhysicalReplicationSlotExists(db *DBConnection, d *schema.ResourceData) (bool, error) { 41 | query := "SELECT 1 FROM pg_catalog.pg_replication_slots WHERE slot_name = $1 and slot_type = 'physical'" 42 | var unused int 43 | err := db.QueryRow(query, d.Id()).Scan(&unused) 44 | switch { 45 | case err == sql.ErrNoRows: 46 | return false, nil 47 | case err != nil: 48 | return false, err 49 | } 50 | 51 | return true, nil 52 | } 53 | 54 | func resourcePostgreSQLPhysicalReplicationSlotRead(db *DBConnection, d *schema.ResourceData) error { 55 | d.Set("name", d.Id()) 56 | return nil 57 | } 58 | 59 | func resourcePostgreSQLPhysicalReplicationSlotDelete(db *DBConnection, d *schema.ResourceData) error { 60 | 61 | replicationSlotName := d.Get("name").(string) 62 | 63 | if _, err := db.Exec("SELECT pg_drop_replication_slot($1)", replicationSlotName); err != nil { 64 | return err 65 | } 66 | 67 | d.SetId("") 68 | return nil 69 | } 70 | -------------------------------------------------------------------------------- /postgresql/resource_postgresql_replication_slot.go: -------------------------------------------------------------------------------- 1 | package postgresql 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "log" 7 | "strings" 8 | 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 10 | ) 11 | 12 | func resourcePostgreSQLReplicationSlot() *schema.Resource { 13 | return &schema.Resource{ 14 | Create: PGResourceFunc(resourcePostgreSQLReplicationSlotCreate), 15 | Read: PGResourceFunc(resourcePostgreSQLReplicationSlotRead), 16 | Delete: PGResourceFunc(resourcePostgreSQLReplicationSlotDelete), 17 | Exists: PGResourceExistsFunc(resourcePostgreSQLReplicationSlotExists), 18 | Importer: &schema.ResourceImporter{ 19 | StateContext: schema.ImportStatePassthroughContext, 20 | }, 21 | 22 | Schema: map[string]*schema.Schema{ 23 | "name": { 24 | Type: schema.TypeString, 25 | Required: true, 26 | ForceNew: true, 27 | }, 28 | "database": { 29 | Type: schema.TypeString, 30 | Optional: true, 31 | Computed: true, 32 | ForceNew: true, 33 | Description: "Sets the database to add the replication slot to", 34 | }, 35 | "plugin": { 36 | Type: schema.TypeString, 37 | Required: true, 38 | ForceNew: true, 39 | Description: "Sets the output plugin to use", 40 | }, 41 | }, 42 | } 43 | } 44 | 45 | func resourcePostgreSQLReplicationSlotCreate(db *DBConnection, d *schema.ResourceData) error { 46 | 47 | name := d.Get("name").(string) 48 | plugin := d.Get("plugin").(string) 49 | databaseName := getDatabaseForReplicationSlot(d, db.client.databaseName) 50 | 51 | txn, err := startTransaction(db.client, databaseName) 52 | if err != nil { 53 | return err 54 | } 55 | defer deferredRollback(txn) 56 | 57 | sql := "SELECT FROM pg_create_logical_replication_slot($1, $2)" 58 | if _, err := txn.Exec(sql, name, plugin); err != nil { 59 | return err 60 | } 61 | 62 | if err = txn.Commit(); err != nil { 63 | return fmt.Errorf("Error creating ReplicationSlot: %w", err) 64 | } 65 | 66 | d.SetId(generateReplicationSlotID(d, databaseName)) 67 | 68 | return resourcePostgreSQLReplicationSlotReadImpl(db, d) 69 | } 70 | 71 | func resourcePostgreSQLReplicationSlotExists(db *DBConnection, d *schema.ResourceData) (bool, error) { 72 | 73 | var ReplicationSlotName string 74 | 75 | database, replicationSlotName, err := getDBReplicationSlotName(d, db.client) 76 | if err != nil { 77 | return false, err 78 | } 79 | 80 | // Check if the database exists 81 | exists, err := dbExists(db, database) 82 | if err != nil || !exists { 83 | return false, err 84 | } 85 | 86 | txn, err := startTransaction(db.client, database) 87 | if err != nil { 88 | return false, err 89 | } 90 | defer deferredRollback(txn) 91 | 92 | query := "SELECT slot_name FROM pg_catalog.pg_replication_slots WHERE slot_name = $1 and database = $2" 93 | err = txn.QueryRow(query, replicationSlotName, database).Scan(&ReplicationSlotName) 94 | switch { 95 | case err == sql.ErrNoRows: 96 | return false, nil 97 | case err != nil: 98 | return false, err 99 | } 100 | 101 | return true, nil 102 | } 103 | 104 | func resourcePostgreSQLReplicationSlotRead(db *DBConnection, d *schema.ResourceData) error { 105 | return resourcePostgreSQLReplicationSlotReadImpl(db, d) 106 | } 107 | 108 | func resourcePostgreSQLReplicationSlotReadImpl(db *DBConnection, d *schema.ResourceData) error { 109 | database, replicationSlotName, err := getDBReplicationSlotName(d, db.client) 110 | if err != nil { 111 | return err 112 | } 113 | 114 | txn, err := startTransaction(db.client, database) 115 | if err != nil { 116 | return err 117 | } 118 | defer deferredRollback(txn) 119 | 120 | var replicationSlotPlugin string 121 | query := `SELECT plugin ` + 122 | `FROM pg_catalog.pg_replication_slots ` + 123 | `WHERE slot_name = $1 AND database = $2` 124 | err = txn.QueryRow(query, replicationSlotName, database).Scan(&replicationSlotPlugin) 125 | switch { 126 | case err == sql.ErrNoRows: 127 | log.Printf("[WARN] PostgreSQL ReplicationSlot (%s) not found for database %s", replicationSlotName, database) 128 | d.SetId("") 129 | return nil 130 | case err != nil: 131 | return fmt.Errorf("Error reading ReplicationSlot: %w", err) 132 | } 133 | 134 | d.Set("name", replicationSlotName) 135 | d.Set("plugin", replicationSlotPlugin) 136 | d.Set("database", database) 137 | d.SetId(generateReplicationSlotID(d, database)) 138 | 139 | return nil 140 | } 141 | 142 | func resourcePostgreSQLReplicationSlotDelete(db *DBConnection, d *schema.ResourceData) error { 143 | 144 | replicationSlotName := d.Get("name").(string) 145 | database := getDatabaseForReplicationSlot(d, db.client.databaseName) 146 | 147 | txn, err := startTransaction(db.client, database) 148 | if err != nil { 149 | return err 150 | } 151 | defer deferredRollback(txn) 152 | 153 | sql := "SELECT pg_drop_replication_slot($1)" 154 | if _, err := txn.Exec(sql, replicationSlotName); err != nil { 155 | return err 156 | } 157 | 158 | if err = txn.Commit(); err != nil { 159 | return fmt.Errorf("Error deleting ReplicationSlot: %w", err) 160 | } 161 | 162 | d.SetId("") 163 | 164 | return nil 165 | } 166 | 167 | func getDatabaseForReplicationSlot(d *schema.ResourceData, databaseName string) string { 168 | if v, ok := d.GetOk("database"); ok { 169 | databaseName = v.(string) 170 | } 171 | 172 | return databaseName 173 | } 174 | 175 | func generateReplicationSlotID(d *schema.ResourceData, databaseName string) string { 176 | return strings.Join([]string{ 177 | databaseName, 178 | d.Get("name").(string), 179 | }, ".") 180 | } 181 | 182 | func getReplicationSlotNameFromID(ID string) string { 183 | splitted := strings.Split(ID, ".") 184 | return splitted[0] 185 | } 186 | 187 | // getDBReplicationSlotName returns database and replication slot name. If we are importing this 188 | // resource, they will be parsed from the resource ID (it will return an error if parsing failed) 189 | // otherwise they will be simply get from the state. 190 | func getDBReplicationSlotName(d *schema.ResourceData, client *Client) (string, string, error) { 191 | database := getDatabaseForReplicationSlot(d, client.databaseName) 192 | replicationSlotName := d.Get("name").(string) 193 | 194 | // When importing, we have to parse the ID to find replication slot and database names. 195 | if replicationSlotName == "" { 196 | parsed := strings.Split(d.Id(), ".") 197 | if len(parsed) != 2 { 198 | return "", "", fmt.Errorf("Replication Slot ID %s has not the expected format 'database.replication_slot': %v", d.Id(), parsed) 199 | } 200 | database = parsed[0] 201 | replicationSlotName = parsed[1] 202 | } 203 | return database, replicationSlotName, nil 204 | } 205 | -------------------------------------------------------------------------------- /postgresql/resource_postgresql_replication_slot_test.go: -------------------------------------------------------------------------------- 1 | package postgresql 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" 10 | ) 11 | 12 | func TestAccPostgresqlReplicationSlot_Basic(t *testing.T) { 13 | resource.Test(t, resource.TestCase{ 14 | PreCheck: func() { 15 | testAccPreCheck(t) 16 | testSuperuserPreCheck(t) 17 | }, 18 | Providers: testAccProviders, 19 | CheckDestroy: testAccCheckPostgresqlReplicationSlotDestroy, 20 | Steps: []resource.TestStep{ 21 | { 22 | Config: ` 23 | resource "postgresql_replication_slot" "myslot" { 24 | name = "slot" 25 | plugin = "test_decoding" 26 | }`, 27 | Check: resource.ComposeTestCheckFunc( 28 | testAccCheckPostgresqlReplicationSlotExists("postgresql_replication_slot.myslot"), 29 | resource.TestCheckResourceAttr( 30 | "postgresql_replication_slot.myslot", "name", "slot"), 31 | resource.TestCheckResourceAttr( 32 | "postgresql_replication_slot.myslot", "plugin", "test_decoding"), 33 | ), 34 | }, 35 | }, 36 | }) 37 | } 38 | 39 | func testAccCheckPostgresqlReplicationSlotDestroy(s *terraform.State) error { 40 | client := testAccProvider.Meta().(*Client) 41 | 42 | for _, rs := range s.RootModule().Resources { 43 | if rs.Type != "postgresql_replication_slot" { 44 | continue 45 | } 46 | 47 | database, ok := rs.Primary.Attributes[extDatabaseAttr] 48 | if !ok { 49 | return fmt.Errorf("No Attribute for database is set") 50 | } 51 | txn, err := startTransaction(client, database) 52 | if err != nil { 53 | return err 54 | } 55 | defer deferredRollback(txn) 56 | 57 | exists, err := checkReplicationSlotExists(txn, getReplicationSlotNameFromID(rs.Primary.ID)) 58 | 59 | if err != nil { 60 | return fmt.Errorf("Error checking replication slot %s", err) 61 | } 62 | 63 | if exists { 64 | return fmt.Errorf("ReplicationSlot still exists after destroy") 65 | } 66 | } 67 | 68 | return nil 69 | } 70 | 71 | func testAccCheckPostgresqlReplicationSlotExists(n string) resource.TestCheckFunc { 72 | return func(s *terraform.State) error { 73 | rs, ok := s.RootModule().Resources[n] 74 | if !ok { 75 | return fmt.Errorf("Resource not found: %s", n) 76 | } 77 | 78 | if rs.Primary.ID == "" { 79 | return fmt.Errorf("No ID is set") 80 | } 81 | 82 | database, ok := rs.Primary.Attributes[extDatabaseAttr] 83 | if !ok { 84 | return fmt.Errorf("No Attribute for database is set") 85 | } 86 | 87 | extName, ok := rs.Primary.Attributes[extNameAttr] 88 | if !ok { 89 | return fmt.Errorf("No Attribute for replication slot name is set") 90 | } 91 | 92 | client := testAccProvider.Meta().(*Client) 93 | txn, err := startTransaction(client, database) 94 | if err != nil { 95 | return err 96 | } 97 | defer deferredRollback(txn) 98 | 99 | exists, err := checkReplicationSlotExists(txn, extName) 100 | 101 | if err != nil { 102 | return fmt.Errorf("Error checking replication slot %s", err) 103 | } 104 | 105 | if !exists { 106 | return fmt.Errorf("ReplicationSlot not found") 107 | } 108 | 109 | return nil 110 | } 111 | } 112 | 113 | func TestAccPostgresqlReplicationSlot_Database(t *testing.T) { 114 | skipIfNotAcc(t) 115 | 116 | dbSuffix, teardown := setupTestDatabase(t, true, true) 117 | defer teardown() 118 | 119 | dbName, _ := getTestDBNames(dbSuffix) 120 | 121 | testAccPostgresqlReplicationSlotDatabaseConfig := fmt.Sprintf(` 122 | resource "postgresql_replication_slot" "myslot" { 123 | name = "slot" 124 | plugin = "test_decoding" 125 | database = "%s" 126 | } 127 | `, dbName) 128 | 129 | resource.Test(t, resource.TestCase{ 130 | PreCheck: func() { 131 | testAccPreCheck(t) 132 | testSuperuserPreCheck(t) 133 | }, 134 | Providers: testAccProviders, 135 | CheckDestroy: testAccCheckPostgresqlReplicationSlotDestroy, 136 | Steps: []resource.TestStep{ 137 | { 138 | Config: testAccPostgresqlReplicationSlotDatabaseConfig, 139 | Check: resource.ComposeTestCheckFunc( 140 | testAccCheckPostgresqlReplicationSlotExists("postgresql_replication_slot.myslot"), 141 | resource.TestCheckResourceAttr( 142 | "postgresql_replication_slot.myslot", "name", "slot"), 143 | resource.TestCheckResourceAttr( 144 | "postgresql_replication_slot.myslot", "plugin", "test_decoding"), 145 | resource.TestCheckResourceAttr( 146 | "postgresql_replication_slot.myslot", "database", dbName), 147 | ), 148 | }, 149 | }, 150 | }) 151 | } 152 | 153 | func checkReplicationSlotExists(txn *sql.Tx, slotName string) (bool, error) { 154 | var _rez bool 155 | err := txn.QueryRow("SELECT TRUE from pg_catalog.pg_replication_slots d WHERE slot_name=$1", slotName).Scan(&_rez) 156 | switch { 157 | case err == sql.ErrNoRows: 158 | return false, nil 159 | case err != nil: 160 | return false, fmt.Errorf("Error reading info about replication slot: %s", err) 161 | } 162 | 163 | return true, nil 164 | } 165 | -------------------------------------------------------------------------------- /postgresql/resource_postgresql_security_label.go: -------------------------------------------------------------------------------- 1 | package postgresql 2 | 3 | import ( 4 | "bytes" 5 | "database/sql" 6 | "fmt" 7 | "log" 8 | "regexp" 9 | "strings" 10 | 11 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 12 | "github.com/lib/pq" 13 | ) 14 | 15 | const ( 16 | securityLabelObjectNameAttr = "object_name" 17 | securityLabelObjectTypeAttr = "object_type" 18 | securityLabelProviderAttr = "label_provider" 19 | securityLabelLabelAttr = "label" 20 | ) 21 | 22 | func resourcePostgreSQLSecurityLabel() *schema.Resource { 23 | return &schema.Resource{ 24 | Create: PGResourceFunc(resourcePostgreSQLSecurityLabelCreate), 25 | Read: PGResourceFunc(resourcePostgreSQLSecurityLabelRead), 26 | Update: PGResourceFunc(resourcePostgreSQLSecurityLabelUpdate), 27 | Delete: PGResourceFunc(resourcePostgreSQLSecurityLabelDelete), 28 | Importer: &schema.ResourceImporter{ 29 | StateContext: schema.ImportStatePassthroughContext, 30 | }, 31 | 32 | Schema: map[string]*schema.Schema{ 33 | securityLabelObjectNameAttr: { 34 | Type: schema.TypeString, 35 | Required: true, 36 | ForceNew: true, 37 | Description: "The name of the existing object to apply the security label to", 38 | }, 39 | securityLabelObjectTypeAttr: { 40 | Type: schema.TypeString, 41 | Required: true, 42 | ForceNew: true, 43 | Description: "The type of the existing object to apply the security label to", 44 | }, 45 | securityLabelProviderAttr: { 46 | Type: schema.TypeString, 47 | Required: true, 48 | ForceNew: true, 49 | Description: "The provider to apply the security label for", 50 | }, 51 | securityLabelLabelAttr: { 52 | Type: schema.TypeString, 53 | Required: true, 54 | ForceNew: false, 55 | Description: "The label to be applied", 56 | }, 57 | }, 58 | } 59 | } 60 | 61 | func resourcePostgreSQLSecurityLabelCreate(db *DBConnection, d *schema.ResourceData) error { 62 | if !db.featureSupported(featureSecurityLabel) { 63 | return fmt.Errorf( 64 | "security Label is not supported for this Postgres version (%s)", 65 | db.version, 66 | ) 67 | } 68 | log.Printf("[DEBUG] PostgreSQL security label Create") 69 | label := d.Get(securityLabelLabelAttr).(string) 70 | if err := resourcePostgreSQLSecurityLabelUpdateImpl(db, d, pq.QuoteLiteral(label)); err != nil { 71 | return err 72 | } 73 | 74 | d.SetId(generateSecurityLabelID(d)) 75 | 76 | return resourcePostgreSQLSecurityLabelReadImpl(db, d) 77 | } 78 | 79 | func resourcePostgreSQLSecurityLabelUpdateImpl(db *DBConnection, d *schema.ResourceData, label string) error { 80 | b := bytes.NewBufferString("SECURITY LABEL ") 81 | 82 | objectType := d.Get(securityLabelObjectTypeAttr).(string) 83 | objectName := d.Get(securityLabelObjectNameAttr).(string) 84 | provider := d.Get(securityLabelProviderAttr).(string) 85 | fmt.Fprint(b, " FOR ", pq.QuoteIdentifier(provider)) 86 | fmt.Fprint(b, " ON ", objectType, pq.QuoteIdentifier(objectName)) 87 | fmt.Fprint(b, " IS ", label) 88 | 89 | if _, err := db.Exec(b.String()); err != nil { 90 | log.Printf("[WARN] PostgreSQL security label Create failed %s", err) 91 | return fmt.Errorf("could not create security label: %w", err) 92 | } 93 | return nil 94 | } 95 | 96 | func resourcePostgreSQLSecurityLabelRead(db *DBConnection, d *schema.ResourceData) error { 97 | if !db.featureSupported(featureSecurityLabel) { 98 | return fmt.Errorf( 99 | "Security Label is not supported for this Postgres version (%s)", 100 | db.version, 101 | ) 102 | } 103 | log.Printf("[DEBUG] PostgreSQL security label Read") 104 | 105 | return resourcePostgreSQLSecurityLabelReadImpl(db, d) 106 | } 107 | 108 | func resourcePostgreSQLSecurityLabelReadImpl(db *DBConnection, d *schema.ResourceData) error { 109 | objectType := d.Get(securityLabelObjectTypeAttr).(string) 110 | objectName := d.Get(securityLabelObjectNameAttr).(string) 111 | provider := d.Get(securityLabelProviderAttr).(string) 112 | 113 | txn, err := startTransaction(db.client, "") 114 | if err != nil { 115 | return err 116 | } 117 | defer deferredRollback(txn) 118 | 119 | query := "SELECT objtype, provider, objname, label FROM pg_seclabels WHERE objtype = $1 and objname = $2 and provider = $3" 120 | row := db.QueryRow(query, objectType, quoteIdentifier(objectName), quoteIdentifier(provider)) 121 | 122 | var label, newObjectName, newProvider string 123 | err = row.Scan(&objectType, &newProvider, &newObjectName, &label) 124 | switch { 125 | case err == sql.ErrNoRows: 126 | log.Printf("[WARN] PostgreSQL security label for (%s '%s') with provider %s not found", objectType, objectName, provider) 127 | d.SetId("") 128 | return nil 129 | case err != nil: 130 | return fmt.Errorf("Error reading security label: %w", err) 131 | } 132 | 133 | if quoteIdentifier(objectName) != newObjectName || quoteIdentifier(provider) != newProvider { 134 | // In reality, this should never happen, but if it does, we want to make sure that the state is in sync with the remote system 135 | // This will trigger a TF error saying that the provider has a bug if it ever happens 136 | objectName = newObjectName 137 | provider = newProvider 138 | } 139 | d.Set(securityLabelObjectTypeAttr, objectType) 140 | d.Set(securityLabelObjectNameAttr, objectName) 141 | d.Set(securityLabelProviderAttr, provider) 142 | d.Set(securityLabelLabelAttr, label) 143 | d.SetId(generateSecurityLabelID(d)) 144 | 145 | return nil 146 | } 147 | 148 | func resourcePostgreSQLSecurityLabelDelete(db *DBConnection, d *schema.ResourceData) error { 149 | if !db.featureSupported(featureSecurityLabel) { 150 | return fmt.Errorf( 151 | "Security Label is not supported for this Postgres version (%s)", 152 | db.version, 153 | ) 154 | } 155 | log.Printf("[DEBUG] PostgreSQL security label Delete") 156 | 157 | if err := resourcePostgreSQLSecurityLabelUpdateImpl(db, d, "NULL"); err != nil { 158 | return err 159 | } 160 | 161 | d.SetId("") 162 | 163 | return nil 164 | } 165 | 166 | func resourcePostgreSQLSecurityLabelUpdate(db *DBConnection, d *schema.ResourceData) error { 167 | if !db.featureSupported(featureServer) { 168 | return fmt.Errorf( 169 | "Security Label is not supported for this Postgres version (%s)", 170 | db.version, 171 | ) 172 | } 173 | log.Printf("[DEBUG] PostgreSQL security label Update") 174 | 175 | label := d.Get(securityLabelLabelAttr).(string) 176 | if err := resourcePostgreSQLSecurityLabelUpdateImpl(db, d, pq.QuoteLiteral(label)); err != nil { 177 | return err 178 | } 179 | 180 | return resourcePostgreSQLSecurityLabelReadImpl(db, d) 181 | } 182 | 183 | func generateSecurityLabelID(d *schema.ResourceData) string { 184 | return strings.Join([]string{ 185 | d.Get(securityLabelProviderAttr).(string), 186 | d.Get(securityLabelObjectTypeAttr).(string), 187 | d.Get(securityLabelObjectNameAttr).(string), 188 | }, ".") 189 | } 190 | 191 | func quoteIdentifier(s string) string { 192 | var result = s 193 | re := regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`) 194 | if !re.MatchString(s) || s != strings.ToLower(s) { 195 | result = pq.QuoteIdentifier(s) 196 | } 197 | return result 198 | } 199 | -------------------------------------------------------------------------------- /postgresql/resource_postgresql_security_label_test.go: -------------------------------------------------------------------------------- 1 | package postgresql 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" 10 | "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" 11 | ) 12 | 13 | func TestAccPostgresqlSecurityLabel_Basic(t *testing.T) { 14 | resource.Test(t, resource.TestCase{ 15 | PreCheck: func() { 16 | testAccPreCheck(t) 17 | testCheckCompatibleVersion(t, featureSecurityLabel) 18 | testSuperuserPreCheck(t) 19 | }, 20 | Providers: testAccProviders, 21 | CheckDestroy: testAccCheckPostgresqlSecurityLabelDestroy, 22 | Steps: []resource.TestStep{ 23 | { 24 | Config: testAccPostgresqlSecurityLabelConfig, 25 | Check: resource.ComposeTestCheckFunc( 26 | testAccCheckPostgresqlSecurityLabelExists("postgresql_security_label.test_label"), 27 | resource.TestCheckResourceAttr( 28 | "postgresql_security_label.test_label", "object_type", "role"), 29 | resource.TestCheckResourceAttr( 30 | "postgresql_security_label.test_label", "object_name", "security_label_test_role"), 31 | resource.TestCheckResourceAttr( 32 | "postgresql_security_label.test_label", "label_provider", "dummy"), 33 | resource.TestCheckResourceAttr( 34 | "postgresql_security_label.test_label", "label", "secret"), 35 | ), 36 | }, 37 | }, 38 | }) 39 | } 40 | 41 | func TestAccPostgresqlSecurityLabel_Update(t *testing.T) { 42 | resource.Test(t, resource.TestCase{ 43 | PreCheck: func() { 44 | testAccPreCheck(t) 45 | testCheckCompatibleVersion(t, featureSecurityLabel) 46 | testSuperuserPreCheck(t) 47 | }, 48 | Providers: testAccProviders, 49 | CheckDestroy: testAccCheckPostgresqlSecurityLabelDestroy, 50 | Steps: []resource.TestStep{ 51 | { 52 | Config: testAccPostgresqlSecurityLabelConfig, 53 | Check: resource.ComposeTestCheckFunc( 54 | testAccCheckPostgresqlSecurityLabelExists("postgresql_security_label.test_label"), 55 | resource.TestCheckResourceAttr( 56 | "postgresql_security_label.test_label", "object_type", "role"), 57 | resource.TestCheckResourceAttr( 58 | "postgresql_security_label.test_label", "object_name", "security_label_test_role"), 59 | resource.TestCheckResourceAttr( 60 | "postgresql_security_label.test_label", "label_provider", "dummy"), 61 | resource.TestCheckResourceAttr( 62 | "postgresql_security_label.test_label", "label", "secret"), 63 | ), 64 | }, 65 | { 66 | Config: testAccPostgresqlSecurityLabelChanges2, 67 | Check: resource.ComposeTestCheckFunc( 68 | testAccCheckPostgresqlSecurityLabelExists("postgresql_security_label.test_label"), 69 | resource.TestCheckResourceAttr( 70 | "postgresql_security_label.test_label", "label", "top secret"), 71 | ), 72 | }, 73 | { 74 | Config: testAccPostgresqlSecurityLabelChanges3, 75 | Check: resource.ComposeTestCheckFunc( 76 | testAccCheckPostgresqlSecurityLabelExists("postgresql_security_label.test_label"), 77 | resource.TestCheckResourceAttr( 78 | "postgresql_security_label.test_label", "object_name", "security_label_test-role2"), 79 | ), 80 | }, 81 | }, 82 | }) 83 | } 84 | 85 | func checkSecurityLabelExists(txn *sql.Tx, objectType string, objectName string, provider string) (bool, error) { 86 | var _rez bool 87 | err := txn.QueryRow("SELECT TRUE FROM pg_seclabels WHERE objtype = $1 AND objname = $2 AND provider = $3", objectType, quoteIdentifier(objectName), provider).Scan(&_rez) 88 | switch { 89 | case err == sql.ErrNoRows: 90 | return false, nil 91 | case err != nil: 92 | return false, fmt.Errorf("Error reading info about security label: %s", err) 93 | } 94 | 95 | return true, nil 96 | } 97 | 98 | func testAccCheckPostgresqlSecurityLabelDestroy(s *terraform.State) error { 99 | client := testAccProvider.Meta().(*Client) 100 | 101 | for _, rs := range s.RootModule().Resources { 102 | if rs.Type != "postgresql_security_label" { 103 | continue 104 | } 105 | 106 | txn, err := startTransaction(client, "") 107 | if err != nil { 108 | return err 109 | } 110 | defer deferredRollback(txn) 111 | 112 | splitted := strings.Split(rs.Primary.ID, ".") 113 | exists, err := checkSecurityLabelExists(txn, splitted[1], splitted[2], splitted[0]) 114 | 115 | if err != nil { 116 | return fmt.Errorf("Error checking security label%s", err) 117 | } 118 | 119 | if exists { 120 | return fmt.Errorf("Security label still exists after destroy") 121 | } 122 | } 123 | 124 | return nil 125 | } 126 | 127 | func testAccCheckPostgresqlSecurityLabelExists(n string) resource.TestCheckFunc { 128 | return func(s *terraform.State) error { 129 | rs, ok := s.RootModule().Resources[n] 130 | if !ok { 131 | return fmt.Errorf("Resource not found: %s", n) 132 | } 133 | 134 | if rs.Primary.ID == "" { 135 | return fmt.Errorf("No ID is set") 136 | } 137 | 138 | objectType, ok := rs.Primary.Attributes[securityLabelObjectTypeAttr] 139 | if !ok { 140 | return fmt.Errorf("No Attribute for object type is set") 141 | } 142 | 143 | objectName, ok := rs.Primary.Attributes[securityLabelObjectNameAttr] 144 | if !ok { 145 | return fmt.Errorf("No Attribute for object name is set") 146 | } 147 | 148 | provider, ok := rs.Primary.Attributes[securityLabelProviderAttr] 149 | if !ok { 150 | return fmt.Errorf("No Attribute for security provider is set") 151 | } 152 | 153 | client := testAccProvider.Meta().(*Client) 154 | txn, err := startTransaction(client, "") 155 | if err != nil { 156 | return err 157 | } 158 | defer deferredRollback(txn) 159 | 160 | exists, err := checkSecurityLabelExists(txn, objectType, objectName, provider) 161 | 162 | if err != nil { 163 | return fmt.Errorf("Error checking security label%s", err) 164 | } 165 | 166 | if !exists { 167 | return fmt.Errorf("Security label not found") 168 | } 169 | 170 | return nil 171 | } 172 | } 173 | 174 | var testAccPostgresqlSecurityLabelConfig = ` 175 | resource "postgresql_role" "test_role" { 176 | name = "security_label_test_role" 177 | login = true 178 | create_database = true 179 | } 180 | resource "postgresql_security_label" "test_label" { 181 | object_type = "role" 182 | object_name = postgresql_role.test_role.name 183 | label_provider = "dummy" 184 | label = "secret" 185 | } 186 | ` 187 | 188 | var testAccPostgresqlSecurityLabelChanges2 = ` 189 | resource "postgresql_role" "test_role" { 190 | name = "security_label_test_role" 191 | login = true 192 | create_database = true 193 | } 194 | resource "postgresql_security_label" "test_label" { 195 | object_type = "role" 196 | object_name = postgresql_role.test_role.name 197 | label_provider = "dummy" 198 | label = "top secret" 199 | } 200 | ` 201 | 202 | var testAccPostgresqlSecurityLabelChanges3 = ` 203 | resource "postgresql_role" "test_role" { 204 | name = "security_label_test-role2" 205 | login = true 206 | create_database = true 207 | } 208 | resource "postgresql_security_label" "test_label" { 209 | object_type = "role" 210 | object_name = postgresql_role.test_role.name 211 | label_provider = "dummy" 212 | label = "top secret" 213 | } 214 | ` 215 | -------------------------------------------------------------------------------- /postgresql/resource_postgresql_user_mapping.go: -------------------------------------------------------------------------------- 1 | package postgresql 2 | 3 | import ( 4 | "bytes" 5 | "database/sql" 6 | "fmt" 7 | "log" 8 | "strings" 9 | 10 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 11 | "github.com/lib/pq" 12 | ) 13 | 14 | const ( 15 | userMappingUserNameAttr = "user_name" 16 | userMappingServerNameAttr = "server_name" 17 | userMappingOptionsAttr = "options" 18 | ) 19 | 20 | func resourcePostgreSQLUserMapping() *schema.Resource { 21 | return &schema.Resource{ 22 | Create: PGResourceFunc(resourcePostgreSQLUserMappingCreate), 23 | Read: PGResourceFunc(resourcePostgreSQLUserMappingRead), 24 | Update: PGResourceFunc(resourcePostgreSQLUserMappingUpdate), 25 | Delete: PGResourceFunc(resourcePostgreSQLUserMappingDelete), 26 | Importer: &schema.ResourceImporter{ 27 | StateContext: schema.ImportStatePassthroughContext, 28 | }, 29 | 30 | Schema: map[string]*schema.Schema{ 31 | userMappingUserNameAttr: { 32 | Type: schema.TypeString, 33 | Required: true, 34 | ForceNew: true, 35 | Description: "The name of an existing user that is mapped to foreign server. CURRENT_ROLE, CURRENT_USER, and USER match the name of the current user. When PUBLIC is specified, a so-called public mapping is created that is used when no user-specific mapping is applicable", 36 | }, 37 | userMappingServerNameAttr: { 38 | Type: schema.TypeString, 39 | Required: true, 40 | ForceNew: true, 41 | Description: "The name of an existing server for which the user mapping is to be created", 42 | }, 43 | userMappingOptionsAttr: { 44 | Type: schema.TypeMap, 45 | Elem: &schema.Schema{ 46 | Type: schema.TypeString, 47 | }, 48 | Optional: true, 49 | Description: "This clause specifies the options of the user mapping. The options typically define the actual user name and password of the mapping. Option names must be unique. The allowed option names and values are specific to the server's foreign-data wrapper", 50 | }, 51 | }, 52 | } 53 | } 54 | 55 | func resourcePostgreSQLUserMappingCreate(db *DBConnection, d *schema.ResourceData) error { 56 | if !db.featureSupported(featureServer) { 57 | return fmt.Errorf( 58 | "Foreign Server resource is not supported for this Postgres version (%s)", 59 | db.version, 60 | ) 61 | } 62 | 63 | username := d.Get(userMappingUserNameAttr).(string) 64 | serverName := d.Get(userMappingServerNameAttr).(string) 65 | 66 | b := bytes.NewBufferString("CREATE USER MAPPING ") 67 | fmt.Fprint(b, " FOR ", pq.QuoteIdentifier(username)) 68 | fmt.Fprint(b, " SERVER ", pq.QuoteIdentifier(serverName)) 69 | 70 | if options, ok := d.GetOk(userMappingOptionsAttr); ok { 71 | fmt.Fprint(b, " OPTIONS ( ") 72 | cnt := 0 73 | len := len(options.(map[string]interface{})) 74 | for k, v := range options.(map[string]interface{}) { 75 | fmt.Fprint(b, " ", pq.QuoteIdentifier(k), " ", pq.QuoteLiteral(v.(string))) 76 | if cnt < len-1 { 77 | fmt.Fprint(b, ", ") 78 | } 79 | cnt++ 80 | } 81 | fmt.Fprint(b, " ) ") 82 | } 83 | 84 | if _, err := db.Exec(b.String()); err != nil { 85 | return fmt.Errorf("Could not create user mapping: %w", err) 86 | } 87 | 88 | d.SetId(generateUserMappingID(d)) 89 | 90 | return resourcePostgreSQLUserMappingReadImpl(db, d) 91 | } 92 | 93 | func resourcePostgreSQLUserMappingRead(db *DBConnection, d *schema.ResourceData) error { 94 | if !db.featureSupported(featureServer) { 95 | return fmt.Errorf( 96 | "Foreign Server resource is not supported for this Postgres version (%s)", 97 | db.version, 98 | ) 99 | } 100 | 101 | return resourcePostgreSQLUserMappingReadImpl(db, d) 102 | } 103 | 104 | func resourcePostgreSQLUserMappingReadImpl(db *DBConnection, d *schema.ResourceData) error { 105 | username := d.Get(userMappingUserNameAttr).(string) 106 | serverName := d.Get(userMappingServerNameAttr).(string) 107 | 108 | txn, err := startTransaction(db.client, "") 109 | if err != nil { 110 | return err 111 | } 112 | defer deferredRollback(txn) 113 | 114 | var userMappingOptions []string 115 | query := "SELECT umoptions FROM information_schema._pg_user_mappings WHERE authorization_identifier = $1 and foreign_server_name = $2" 116 | err = txn.QueryRow(query, username, serverName).Scan(pq.Array(&userMappingOptions)) 117 | 118 | if err != sql.ErrNoRows && err != nil { 119 | // Fallback to pg_user_mappings table if information_schema._pg_user_mappings is not available 120 | query := "SELECT umoptions FROM pg_user_mappings WHERE usename = $1 and srvname = $2" 121 | err = txn.QueryRow(query, username, serverName).Scan(pq.Array(&userMappingOptions)) 122 | } 123 | 124 | switch { 125 | case err == sql.ErrNoRows: 126 | log.Printf("[WARN] PostgreSQL user mapping (%s) for server (%s) not found", username, serverName) 127 | d.SetId("") 128 | return nil 129 | case err != nil: 130 | return fmt.Errorf("Error reading user mapping: %w", err) 131 | } 132 | 133 | mappedOptions := make(map[string]interface{}) 134 | for _, v := range userMappingOptions { 135 | pair := strings.SplitN(v, "=", 2) 136 | mappedOptions[pair[0]] = pair[1] 137 | } 138 | 139 | d.Set(userMappingUserNameAttr, username) 140 | d.Set(userMappingServerNameAttr, serverName) 141 | d.Set(userMappingOptionsAttr, mappedOptions) 142 | d.SetId(generateUserMappingID(d)) 143 | 144 | return nil 145 | } 146 | 147 | func resourcePostgreSQLUserMappingDelete(db *DBConnection, d *schema.ResourceData) error { 148 | if !db.featureSupported(featureServer) { 149 | return fmt.Errorf( 150 | "Foreign Server resource is not supported for this Postgres version (%s)", 151 | db.version, 152 | ) 153 | } 154 | 155 | username := d.Get(userMappingUserNameAttr).(string) 156 | serverName := d.Get(userMappingServerNameAttr).(string) 157 | 158 | txn, err := startTransaction(db.client, "") 159 | if err != nil { 160 | return err 161 | } 162 | defer deferredRollback(txn) 163 | 164 | sql := fmt.Sprintf("DROP USER MAPPING FOR %s SERVER %s ", pq.QuoteIdentifier(username), pq.QuoteIdentifier(serverName)) 165 | if _, err := txn.Exec(sql); err != nil { 166 | return err 167 | } 168 | 169 | if err = txn.Commit(); err != nil { 170 | return fmt.Errorf("Error deleting user mapping: %w", err) 171 | } 172 | 173 | d.SetId("") 174 | 175 | return nil 176 | } 177 | 178 | func resourcePostgreSQLUserMappingUpdate(db *DBConnection, d *schema.ResourceData) error { 179 | if !db.featureSupported(featureServer) { 180 | return fmt.Errorf( 181 | "Foreign Server resource is not supported for this Postgres version (%s)", 182 | db.version, 183 | ) 184 | } 185 | 186 | if err := setUserMappingOptionsIfChanged(db, d); err != nil { 187 | return err 188 | } 189 | 190 | return resourcePostgreSQLUserMappingReadImpl(db, d) 191 | } 192 | 193 | func setUserMappingOptionsIfChanged(db *DBConnection, d *schema.ResourceData) error { 194 | if !d.HasChange(userMappingOptionsAttr) { 195 | return nil 196 | } 197 | 198 | username := d.Get(userMappingUserNameAttr).(string) 199 | serverName := d.Get(userMappingServerNameAttr).(string) 200 | 201 | b := bytes.NewBufferString("ALTER USER MAPPING ") 202 | fmt.Fprintf(b, " FOR %s SERVER %s ", pq.QuoteIdentifier(username), pq.QuoteIdentifier(serverName)) 203 | 204 | oldOptions, newOptions := d.GetChange(userMappingOptionsAttr) 205 | fmt.Fprint(b, " OPTIONS ( ") 206 | cnt := 0 207 | len := len(newOptions.(map[string]interface{})) 208 | toRemove := oldOptions.(map[string]interface{}) 209 | for k, v := range newOptions.(map[string]interface{}) { 210 | operation := "ADD" 211 | if oldOptions.(map[string]interface{})[k] != nil { 212 | operation = "SET" 213 | delete(toRemove, k) 214 | } 215 | fmt.Fprintf(b, " %s %s %s ", operation, pq.QuoteIdentifier(k), pq.QuoteLiteral(v.(string))) 216 | if cnt < len-1 { 217 | fmt.Fprint(b, ", ") 218 | } 219 | cnt++ 220 | } 221 | 222 | for k := range toRemove { 223 | if cnt != 0 { // starting with 0 means to drop all the options. Cannot start with comma 224 | fmt.Fprint(b, " , ") 225 | } 226 | fmt.Fprintf(b, " DROP %s ", pq.QuoteIdentifier(k)) 227 | cnt++ 228 | } 229 | 230 | fmt.Fprint(b, " ) ") 231 | 232 | if _, err := db.Exec(b.String()); err != nil { 233 | return fmt.Errorf("Error updating user mapping options: %w", err) 234 | } 235 | 236 | return nil 237 | } 238 | 239 | func generateUserMappingID(d *schema.ResourceData) string { 240 | return strings.Join([]string{ 241 | d.Get(userMappingUserNameAttr).(string), 242 | d.Get(userMappingServerNameAttr).(string), 243 | }, ".") 244 | } 245 | -------------------------------------------------------------------------------- /postgresql/resource_postgresql_user_mapping_test.go: -------------------------------------------------------------------------------- 1 | package postgresql 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" 10 | "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" 11 | ) 12 | 13 | func TestAccPostgresqlUserMapping_Basic(t *testing.T) { 14 | resource.Test(t, resource.TestCase{ 15 | PreCheck: func() { 16 | testAccPreCheck(t) 17 | testCheckCompatibleVersion(t, featureServer) 18 | testSuperuserPreCheck(t) 19 | }, 20 | Providers: testAccProviders, 21 | CheckDestroy: testAccCheckPostgresqlUserMappingDestroy, 22 | Steps: []resource.TestStep{ 23 | { 24 | Config: testAccPostgresqlUserMappingConfig, 25 | Check: resource.ComposeTestCheckFunc( 26 | testAccCheckPostgresqlUserMappingExists("postgresql_user_mapping.remote"), 27 | resource.TestCheckResourceAttr( 28 | "postgresql_user_mapping.remote", "server_name", "myserver_postgres"), 29 | resource.TestCheckResourceAttr( 30 | "postgresql_user_mapping.remote", "user_name", "remote"), 31 | resource.TestCheckResourceAttr( 32 | "postgresql_user_mapping.remote", "options.user", "admin"), 33 | resource.TestCheckResourceAttr( 34 | "postgresql_user_mapping.remote", "options.password", "pass"), 35 | resource.TestCheckResourceAttr( 36 | "postgresql_user_mapping.special_chars", "options.password", "pass=$*'"), 37 | ), 38 | }, 39 | }, 40 | }) 41 | } 42 | 43 | func TestAccPostgresqlUserMapping_Update(t *testing.T) { 44 | resource.Test(t, resource.TestCase{ 45 | PreCheck: func() { 46 | testAccPreCheck(t) 47 | testCheckCompatibleVersion(t, featureServer) 48 | testSuperuserPreCheck(t) 49 | }, 50 | Providers: testAccProviders, 51 | CheckDestroy: testAccCheckPostgresqlUserMappingDestroy, 52 | Steps: []resource.TestStep{ 53 | { 54 | Config: testAccPostgresqlUserMappingConfig, 55 | Check: resource.ComposeTestCheckFunc( 56 | testAccCheckPostgresqlServerExists("postgresql_user_mapping.remote"), 57 | resource.TestCheckResourceAttr( 58 | "postgresql_user_mapping.remote", "server_name", "myserver_postgres"), 59 | resource.TestCheckResourceAttr( 60 | "postgresql_user_mapping.remote", "options.password", "pass"), 61 | ), 62 | }, 63 | { 64 | Config: testAccPostgresqlUserMappingChanges2, 65 | Check: resource.ComposeTestCheckFunc( 66 | testAccCheckPostgresqlServerExists("postgresql_user_mapping.remote"), 67 | resource.TestCheckResourceAttr( 68 | "postgresql_user_mapping.remote", "options.password", "passUpdated"), 69 | ), 70 | }, 71 | { 72 | Config: testAccPostgresqlUserMappingChanges3, 73 | Check: resource.ComposeTestCheckFunc( 74 | testAccCheckPostgresqlServerExists("postgresql_user_mapping.remote"), 75 | resource.TestCheckResourceAttr( 76 | "postgresql_user_mapping.remote", "options.%", "0"), 77 | ), 78 | }, 79 | }, 80 | }) 81 | } 82 | 83 | func checkUserMappingExists(txn *sql.Tx, username string, serverName string) (bool, error) { 84 | var _rez bool 85 | err := txn.QueryRow("SELECT TRUE FROM pg_user_mappings WHERE usename = $1 AND srvname = $2", username, serverName).Scan(&_rez) 86 | switch { 87 | case err == sql.ErrNoRows: 88 | return false, nil 89 | case err != nil: 90 | return false, fmt.Errorf("Error reading info about user mapping: %s", err) 91 | } 92 | 93 | return true, nil 94 | } 95 | 96 | func testAccCheckPostgresqlUserMappingDestroy(s *terraform.State) error { 97 | client := testAccProvider.Meta().(*Client) 98 | 99 | for _, rs := range s.RootModule().Resources { 100 | if rs.Type != "postgresql_user_mapping" { 101 | continue 102 | } 103 | 104 | txn, err := startTransaction(client, "") 105 | if err != nil { 106 | return err 107 | } 108 | defer deferredRollback(txn) 109 | 110 | splitted := strings.Split(rs.Primary.ID, ".") 111 | exists, err := checkUserMappingExists(txn, splitted[0], splitted[1]) 112 | 113 | if err != nil { 114 | return fmt.Errorf("Error checking user mapping %s", err) 115 | } 116 | 117 | if exists { 118 | return fmt.Errorf("User mapping still exists after destroy") 119 | } 120 | } 121 | 122 | return nil 123 | } 124 | 125 | func testAccCheckPostgresqlUserMappingExists(n string) resource.TestCheckFunc { 126 | return func(s *terraform.State) error { 127 | rs, ok := s.RootModule().Resources[n] 128 | if !ok { 129 | return fmt.Errorf("Resource not found: %s", n) 130 | } 131 | 132 | if rs.Primary.ID == "" { 133 | return fmt.Errorf("No ID is set") 134 | } 135 | 136 | username, ok := rs.Primary.Attributes[userMappingUserNameAttr] 137 | if !ok { 138 | return fmt.Errorf("No Attribute for username is set") 139 | } 140 | 141 | serverName, ok := rs.Primary.Attributes[userMappingServerNameAttr] 142 | if !ok { 143 | return fmt.Errorf("No Attribute for server name is set") 144 | } 145 | 146 | client := testAccProvider.Meta().(*Client) 147 | txn, err := startTransaction(client, "") 148 | if err != nil { 149 | return err 150 | } 151 | defer deferredRollback(txn) 152 | 153 | exists, err := checkUserMappingExists(txn, username, serverName) 154 | 155 | if err != nil { 156 | return fmt.Errorf("Error checking user mapping %s", err) 157 | } 158 | 159 | if !exists { 160 | return fmt.Errorf("User mapping not found") 161 | } 162 | 163 | return nil 164 | } 165 | } 166 | 167 | var testAccPostgresqlUserMappingConfig = ` 168 | resource "postgresql_extension" "ext_postgres_fdw" { 169 | name = "postgres_fdw" 170 | } 171 | 172 | resource "postgresql_server" "myserver_postgres" { 173 | server_name = "myserver_postgres" 174 | fdw_name = "postgres_fdw" 175 | options = { 176 | host = "foo" 177 | dbname = "foodb" 178 | port = "5432" 179 | } 180 | 181 | depends_on = [postgresql_extension.ext_postgres_fdw] 182 | } 183 | 184 | resource "postgresql_role" "remote" { 185 | name = "remote" 186 | } 187 | 188 | resource "postgresql_user_mapping" "remote" { 189 | server_name = postgresql_server.myserver_postgres.server_name 190 | user_name = postgresql_role.remote.name 191 | options = { 192 | user = "admin" 193 | password = "pass" 194 | } 195 | } 196 | 197 | resource "postgresql_role" "special" { 198 | name = "special" 199 | } 200 | 201 | resource "postgresql_user_mapping" "special_chars" { 202 | server_name = postgresql_server.myserver_postgres.server_name 203 | user_name = postgresql_role.special.name 204 | options = { 205 | user = "admin" 206 | password = "pass=$*'" 207 | } 208 | } 209 | ` 210 | 211 | var testAccPostgresqlUserMappingChanges2 = ` 212 | resource "postgresql_extension" "ext_postgres_fdw" { 213 | name = "postgres_fdw" 214 | } 215 | 216 | resource "postgresql_server" "myserver_postgres" { 217 | server_name = "myserver_postgres" 218 | fdw_name = "postgres_fdw" 219 | options = { 220 | host = "foo" 221 | dbname = "foodb" 222 | port = "5432" 223 | } 224 | 225 | depends_on = [postgresql_extension.ext_postgres_fdw] 226 | } 227 | 228 | resource "postgresql_role" "remote" { 229 | name = "remote" 230 | } 231 | 232 | resource "postgresql_user_mapping" "remote" { 233 | server_name = postgresql_server.myserver_postgres.server_name 234 | user_name = postgresql_role.remote.name 235 | options = { 236 | user = "admin" 237 | password = "passUpdated" 238 | } 239 | } 240 | ` 241 | 242 | var testAccPostgresqlUserMappingChanges3 = ` 243 | resource "postgresql_extension" "ext_postgres_fdw" { 244 | name = "postgres_fdw" 245 | } 246 | 247 | resource "postgresql_server" "myserver_postgres" { 248 | server_name = "myserver_postgres" 249 | fdw_name = "postgres_fdw" 250 | options = { 251 | host = "foo" 252 | dbname = "foodb" 253 | port = "5432" 254 | } 255 | 256 | depends_on = [postgresql_extension.ext_postgres_fdw] 257 | } 258 | 259 | resource "postgresql_role" "remote" { 260 | name = "remote" 261 | } 262 | 263 | resource "postgresql_user_mapping" "remote" { 264 | server_name = postgresql_server.myserver_postgres.server_name 265 | user_name = postgresql_role.remote.name 266 | } 267 | ` 268 | -------------------------------------------------------------------------------- /scripts/changelog-links.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script rewrites [GH-nnnn]-style references in the CHANGELOG.md file to 4 | # be Markdown links to the given github issues. 5 | # 6 | # This is run during releases so that the issue references in all of the 7 | # released items are presented as clickable links, but we can just use the 8 | # easy [GH-nnnn] shorthand for quickly adding items to the "Unrelease" section 9 | # while merging things between releases. 10 | 11 | set -e 12 | 13 | if [[ ! -f CHANGELOG.md ]]; then 14 | echo "ERROR: CHANGELOG.md not found in pwd." 15 | echo "Please run this from the root of the terraform provider repository" 16 | exit 1 17 | fi 18 | 19 | if [[ `uname` == "Darwin" ]]; then 20 | echo "Using BSD sed" 21 | SED="sed -i.bak -E -e" 22 | else 23 | echo "Using GNU sed" 24 | SED="sed -i.bak -r -e" 25 | fi 26 | 27 | PROVIDER_URL="https:\/\/github.com\/terraform-providers\/terraform-provider-postgresql\/issues" 28 | 29 | $SED "s/GH-([0-9]+)/\[#\1\]\($PROVIDER_URL\/\1\)/g" -e 's/\[\[#(.+)([0-9])\)]$/(\[#\1\2))/g' CHANGELOG.md 30 | 31 | rm CHANGELOG.md.bak 32 | -------------------------------------------------------------------------------- /scripts/gofmtcheck.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Check gofmt 4 | echo "==> Checking that code complies with gofmt requirements..." 5 | gofmt_files=$(gofmt -l `find . -name '*.go' | grep -v vendor`) 6 | if [[ -n ${gofmt_files} ]]; then 7 | echo 'gofmt needs running on the following files:' 8 | echo "${gofmt_files}" 9 | echo "You can use the command: \`make fmt\` to reformat code." 10 | exit 1 11 | fi 12 | 13 | exit 0 14 | -------------------------------------------------------------------------------- /scripts/gogetcookie.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | touch ~/.gitcookies 4 | chmod 0600 ~/.gitcookies 5 | 6 | git config --global http.cookiefile ~/.gitcookies 7 | 8 | tr , \\t <<\__END__ >>~/.gitcookies 9 | .googlesource.com,TRUE,/,TRUE,2147483647,o,git-paul.hashicorp.com=1/z7s05EYPudQ9qoe6dMVfmAVwgZopEkZBb1a2mA5QtHE 10 | __END__ 11 | -------------------------------------------------------------------------------- /tests/build/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG PGVERSION 2 | FROM postgres:${PGVERSION:-latest} 3 | 4 | ARG PGVERSION 5 | RUN apt-get update && apt-get install -y build-essential postgresql-server-dev-${PGVERSION:-all} 6 | RUN dpkg -l |grep postgresql 7 | COPY dummy_seclabel /opt/dummy_seclabel 8 | WORKDIR /opt/dummy_seclabel 9 | RUN make 10 | -------------------------------------------------------------------------------- /tests/build/dummy_seclabel/Makefile: -------------------------------------------------------------------------------- 1 | # src/test/modules/dummy_seclabel/Makefile 2 | 3 | MODULES = dummy_seclabel 4 | PGFILEDESC = "dummy_seclabel - regression testing of the SECURITY LABEL statement" 5 | 6 | EXTENSION = dummy_seclabel 7 | DATA = dummy_seclabel--1.0.sql 8 | 9 | REGRESS = dummy_seclabel 10 | 11 | PG_CONFIG = pg_config 12 | PGXS := $(shell $(PG_CONFIG) --pgxs) 13 | include $(PGXS) 14 | -------------------------------------------------------------------------------- /tests/build/dummy_seclabel/dummy_seclabel--1.0.sql: -------------------------------------------------------------------------------- 1 | /* src/test/modules/dummy_seclabel/dummy_seclabel--1.0.sql */ 2 | 3 | -- complain if script is sourced in psql, rather than via CREATE EXTENSION 4 | \echo Use "CREATE EXTENSION dummy_seclabel" to load this file. \quit 5 | 6 | CREATE FUNCTION dummy_seclabel_dummy() 7 | RETURNS pg_catalog.void 8 | AS 'MODULE_PATHNAME' LANGUAGE C; 9 | -------------------------------------------------------------------------------- /tests/build/dummy_seclabel/dummy_seclabel.c: -------------------------------------------------------------------------------- 1 | /* 2 | * dummy_seclabel.c 3 | * 4 | * Dummy security label provider. 5 | * 6 | * This module does not provide anything worthwhile from a security 7 | * perspective, but allows regression testing independent of platform-specific 8 | * features like SELinux. 9 | * 10 | * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group 11 | * Portions Copyright (c) 1994, Regents of the University of California 12 | */ 13 | #include "postgres.h" 14 | 15 | #include "commands/seclabel.h" 16 | #include "fmgr.h" 17 | #include "miscadmin.h" 18 | #include "utils/rel.h" 19 | 20 | PG_MODULE_MAGIC; 21 | 22 | PG_FUNCTION_INFO_V1(dummy_seclabel_dummy); 23 | 24 | static void 25 | dummy_object_relabel(const ObjectAddress *object, const char *seclabel) 26 | { 27 | if (seclabel == NULL || 28 | strcmp(seclabel, "unclassified") == 0 || 29 | strcmp(seclabel, "classified") == 0) 30 | return; 31 | 32 | if (strcmp(seclabel, "secret") == 0 || 33 | strcmp(seclabel, "top secret") == 0) 34 | { 35 | if (!superuser()) 36 | ereport(ERROR, 37 | (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), 38 | errmsg("only superuser can set '%s' label", seclabel))); 39 | return; 40 | } 41 | ereport(ERROR, 42 | (errcode(ERRCODE_INVALID_NAME), 43 | errmsg("'%s' is not a valid security label", seclabel))); 44 | } 45 | 46 | void 47 | _PG_init(void) 48 | { 49 | register_label_provider("dummy", dummy_object_relabel); 50 | } 51 | 52 | /* 53 | * This function is here just so that the extension is not completely empty 54 | * and the dynamic library is loaded when CREATE EXTENSION runs. 55 | */ 56 | Datum 57 | dummy_seclabel_dummy(PG_FUNCTION_ARGS) 58 | { 59 | PG_RETURN_VOID(); 60 | } 61 | -------------------------------------------------------------------------------- /tests/build/dummy_seclabel/dummy_seclabel.control: -------------------------------------------------------------------------------- 1 | comment = 'Test code for SECURITY LABEL feature' 2 | default_version = '1.0' 3 | module_pathname = '$libdir/dummy_seclabel' 4 | relocatable = true 5 | -------------------------------------------------------------------------------- /tests/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | postgres: 5 | build: 6 | context: build 7 | args: 8 | - PGVERSION=${PGVERSION} 9 | user: postgres 10 | command: 11 | - "postgres" 12 | - "-c" 13 | - "wal_level=logical" 14 | - "-c" 15 | - "max_replication_slots=10" 16 | - "-c" 17 | - "shared_preload_libraries=/opt/dummy_seclabel/dummy_seclabel" 18 | environment: 19 | POSTGRES_PASSWORD: ${PGPASSWORD} 20 | ports: 21 | - 25432:5432 22 | healthcheck: 23 | test: [ "CMD-SHELL", "pg_isready" ] 24 | interval: 10s 25 | timeout: 5s 26 | retries: 5 27 | -------------------------------------------------------------------------------- /tests/switch_rds.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | HERE=$(dirname $(readlink -f ${BASH_SOURCE:-${(%):-%N}})) 4 | 5 | source "$HERE/switch_superuser.sh" 6 | 7 | echo "Switching to an RDS-like environment" 8 | psql -d postgres > /dev/null < /dev/null < $1 " 7 | echo "####################" 8 | } 9 | 10 | setup() { 11 | "$(pwd)"/tests/testacc_setup.sh 12 | } 13 | 14 | run() { 15 | go test -count=1 ./postgresql -v -timeout 120m 16 | 17 | # keep the return value for the scripts to fail and clean properly 18 | return $? 19 | } 20 | 21 | cleanup() { 22 | "$(pwd)"/tests/testacc_cleanup.sh 23 | } 24 | 25 | run_suite() { 26 | suite=${1?} 27 | log "setup ($1)" && setup 28 | source "./tests/switch_$suite.sh" 29 | log "run ($1)" && run || (log "cleanup" && cleanup && exit 1) 30 | log "cleanup ($1)" && cleanup 31 | } 32 | 33 | run_suite "superuser" 34 | run_suite "rds" 35 | -------------------------------------------------------------------------------- /tests/testacc_setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source "$(pwd)"/tests/switch_superuser.sh 4 | docker compose -f "$(pwd)"/tests/docker-compose.yml up -d --wait 5 | -------------------------------------------------------------------------------- /website/docs/d/postgresql_schemas.html.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: "postgresql" 3 | page_title: "PostgreSQL: postgresql_schemas" 4 | sidebar_current: "docs-postgresql-data-source-postgresql_schemas" 5 | description: |- 6 | Retrieves a list of schema names from a PostgreSQL database. 7 | --- 8 | 9 | # postgresql\_schemas 10 | 11 | The ``postgresql_schemas`` data source retrieves a list of schema names from a specified PostgreSQL database. 12 | 13 | 14 | ## Usage 15 | 16 | ```hcl 17 | data "postgresql_schemas" "my_schemas" { 18 | database = "my_database" 19 | } 20 | 21 | ``` 22 | 23 | ## Argument Reference 24 | 25 | * `database` - (Required) The PostgreSQL database which will be queried for schema names. 26 | * `include_system_schemas` - (Optional) Determines whether to include system schemas (pg_ prefix and information_schema). 'public' will always be included. Defaults to ``false``. 27 | * `like_any_patterns` - (Optional) List of expressions which will be pattern matched in the query using the PostgreSQL ``LIKE ANY`` operators. 28 | * `like_all_patterns` - (Optional) List of expressions which will be pattern matched in the query using the PostgreSQL ``LIKE ALL`` operators. 29 | * `not_like_all_patterns` - (Optional) List of expressions which will be pattern matched in the query using the PostgreSQL ``NOT LIKE ALL`` operators. 30 | * `regex_pattern` - (Optional) Expression which will be pattern matched in the query using the PostgreSQL ``~`` (regular expression match) operator. 31 | 32 | Note that all optional arguments can be used in conjunction. 33 | 34 | ## Attributes Reference 35 | 36 | * `schemas` - A list of full names of found schemas. 37 | -------------------------------------------------------------------------------- /website/docs/d/postgresql_sequences.html.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: "postgresql" 3 | page_title: "PostgreSQL: postgresql_sequences" 4 | sidebar_current: "docs-postgresql-data-source-postgresql_sequences" 5 | description: |- 6 | Retrieves a list of sequence names from a PostgreSQL database. 7 | --- 8 | 9 | # postgresql\_sequences 10 | 11 | The ``postgresql_sequences`` data source retrieves a list of sequence names from a specified PostgreSQL database. 12 | 13 | 14 | ## Usage 15 | 16 | ```hcl 17 | data "postgresql_sequences" "my_sequences" { 18 | database = "my_database" 19 | } 20 | 21 | ``` 22 | 23 | ## Argument Reference 24 | 25 | * `database` - (Required) The PostgreSQL database which will be queried for sequence names. 26 | * `schemas` - (Optional) List of PostgreSQL schema(s) which will be queried for sequence names. Queries all schemas in the database by default. 27 | * `like_any_patterns` - (Optional) List of expressions which will be pattern matched against sequence names in the query using the PostgreSQL ``LIKE ANY`` operators. 28 | * `like_all_patterns` - (Optional) List of expressions which will be pattern matched against sequence names in the query using the PostgreSQL ``LIKE ALL`` operators. 29 | * `not_like_all_patterns` - (Optional) List of expressions which will be pattern matched against sequence names in the query using the PostgreSQL ``NOT LIKE ALL`` operators. 30 | * `regex_pattern` - (Optional) Expression which will be pattern matched against sequence names in the query using the PostgreSQL ``~`` (regular expression match) operator. 31 | 32 | Note that all optional arguments can be used in conjunction. 33 | 34 | ## Attributes Reference 35 | 36 | * `sequences` - A list of PostgreSQL sequences retrieved by this data source. Each sequence consists of the fields documented below. 37 | ___ 38 | 39 | The `sequence` block consists of: 40 | 41 | * `object_name` - The sequence name. 42 | 43 | * `schema_name` - The parent schema. 44 | 45 | * `data_type` - The sequence's data type as defined in ``information_schema.sequences``. 46 | -------------------------------------------------------------------------------- /website/docs/d/postgresql_tables.html.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: "postgresql" 3 | page_title: "PostgreSQL: postgresql_tables" 4 | sidebar_current: "docs-postgresql-data-source-postgresql_tables" 5 | description: |- 6 | Retrieves a list of table names from a PostgreSQL database. 7 | --- 8 | 9 | # postgresql\_tables 10 | 11 | The ``postgresql_tables`` data source retrieves a list of table names from a specified PostgreSQL database. 12 | 13 | 14 | ## Usage 15 | 16 | ```hcl 17 | data "postgresql_tables" "my_tables" { 18 | database = "my_database" 19 | } 20 | 21 | ``` 22 | 23 | ## Argument Reference 24 | 25 | * `database` - (Required) The PostgreSQL database which will be queried for table names. 26 | * `schemas` - (Optional) List of PostgreSQL schema(s) which will be queried for table names. Queries all schemas in the database by default. 27 | * `table_types` - (Optional) List of PostgreSQL table types which will be queried for table names. Includes all table types by default (including views and temp tables). Use 'BASE TABLE' for normal tables only. 28 | * `like_any_patterns` - (Optional) List of expressions which will be pattern matched against table names in the query using the PostgreSQL ``LIKE ANY`` operators. 29 | * `like_all_patterns` - (Optional) List of expressions which will be pattern matched against table names in the query using the PostgreSQL ``LIKE ALL`` operators. 30 | * `not_like_all_patterns` - (Optional) List of expressions which will be pattern matched against table names in the query using the PostgreSQL ``NOT LIKE ALL`` operators. 31 | * `regex_pattern` - (Optional) Expression which will be pattern matched against table names in the query using the PostgreSQL ``~`` (regular expression match) operator. 32 | 33 | Note that all optional arguments can be used in conjunction. 34 | 35 | ## Attributes Reference 36 | 37 | * `tables` - A list of PostgreSQL tables retrieved by this data source. Each table consists of the fields documented below. 38 | ___ 39 | 40 | The `tables` block consists of: 41 | 42 | * `object_name` - The table name. 43 | 44 | * `schema_name` - The parent schema. 45 | 46 | * `table_type` - The table type as defined in ``information_schema.tables``. 47 | 48 | -------------------------------------------------------------------------------- /website/docs/r/postgresql_database.html.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: "postgresql" 3 | page_title: "PostgreSQL: postgresql_database" 4 | sidebar_current: "docs-postgresql-resource-postgresql_database" 5 | description: |- 6 | Creates and manages a database on a PostgreSQL server. 7 | --- 8 | 9 | # postgresql\_database 10 | 11 | The ``postgresql_database`` resource creates and manages [database 12 | objects](https://www.postgresql.org/docs/current/static/managing-databases.html) 13 | within a PostgreSQL server instance. 14 | 15 | 16 | ## Usage 17 | 18 | ```hcl 19 | resource "postgresql_database" "my_db" { 20 | name = "my_db" 21 | owner = "my_role" 22 | template = "template0" 23 | lc_collate = "C" 24 | connection_limit = -1 25 | allow_connections = true 26 | alter_object_ownership = true 27 | } 28 | ``` 29 | 30 | ## Argument Reference 31 | 32 | * `name` - (Required) The name of the database. Must be unique on the PostgreSQL 33 | server instance where it is configured. 34 | 35 | * `owner` - (Optional) The role name of the user who will own the database, or 36 | `DEFAULT` to use the default (namely, the user executing the command). To 37 | create a database owned by another role or to change the owner of an existing 38 | database, you must be a direct or indirect member of the specified role, or 39 | the username in the provider is a superuser. 40 | 41 | * `tablespace_name` - (Optional) The name of the tablespace that will be 42 | associated with the database, or `DEFAULT` to use the template database's 43 | tablespace. This tablespace will be the default tablespace used for objects 44 | created in this database. 45 | 46 | * `connection_limit` - (Optional) How many concurrent connections can be 47 | established to this database. `-1` (the default) means no limit. 48 | 49 | * `allow_connections` - (Optional) If `false` then no one can connect to this 50 | database. The default is `true`, allowing connections (except as restricted by 51 | other mechanisms, such as `GRANT` or `REVOKE CONNECT`). 52 | 53 | * `is_template` - (Optional) If `true`, then this database can be cloned by any 54 | user with `CREATEDB` privileges; if `false` (the default), then only 55 | superusers or the owner of the database can clone it. 56 | 57 | * `template` - (Optional) The name of the template database from which to create 58 | the database, or `DEFAULT` to use the default template (`template0`). NOTE: 59 | the default in Terraform is `template0`, not `template1`. Changing this value 60 | will force the creation of a new resource as this value can only be changed 61 | when a database is created. 62 | 63 | * `encoding` - (Optional) Character set encoding to use in the database. 64 | Specify a string constant (e.g. `UTF8` or `SQL_ASCII`), or an integer encoding 65 | number. If unset or set to an empty string the default encoding is set to 66 | `UTF8`. If set to `DEFAULT` Terraform will use the same encoding as the 67 | template database. Changing this value will force the creation of a new 68 | resource as this value can only be changed when a database is created. 69 | 70 | * `lc_collate` - (Optional) Collation order (`LC_COLLATE`) to use in the 71 | database. This affects the sort order applied to strings, e.g. in queries 72 | with `ORDER BY`, as well as the order used in indexes on text columns. If 73 | unset or set to an empty string the default collation is set to `C`. If set 74 | to `DEFAULT` Terraform will use the same collation order as the specified 75 | `template` database. Changing this value will force the creation of a new 76 | resource as this value can only be changed when a database is created. 77 | 78 | * `lc_ctype` - (Optional) Character classification (`LC_CTYPE`) to use in the 79 | database. This affects the categorization of characters, e.g. lower, upper and 80 | digit. If unset or set to an empty string the default character classification 81 | is set to `C`. If set to `DEFAULT` Terraform will use the character 82 | classification of the specified `template` database. Changing this value will 83 | force the creation of a new resource as this value can only be changed when a 84 | database is created. 85 | 86 | * `alter_object_ownership` - (Optional) If `true`, the change of the database 87 | `owner` will also include a reassignment of the ownership of preexisting 88 | objects like tables or sequences from the previous owner to the new one. 89 | If set to `false` (the default), then the previous database `owner` will still 90 | hold the ownership of the objects in that database. To alter existing objects in 91 | the database, you must be a direct or indirect member of the specified role, or 92 | the username in the provider must be superuser. 93 | 94 | ## Import Example 95 | 96 | `postgresql_database` supports importing resources. Supposing the following 97 | Terraform: 98 | 99 | ```hcl 100 | provider "postgresql" { 101 | alias = "admindb" 102 | } 103 | 104 | resource "postgresql_database" "db1" { 105 | provider = "postgresql.admindb" 106 | 107 | name = "testdb1" 108 | } 109 | ``` 110 | 111 | It is possible to import a `postgresql_database` resource with the following 112 | command: 113 | 114 | ``` 115 | $ terraform import postgresql_database.db1 testdb1 116 | ``` 117 | 118 | Where `testdb1` is the name of the database to import and 119 | `postgresql_database.db1` is the name of the resource whose state will be 120 | populated as a result of the command. 121 | -------------------------------------------------------------------------------- /website/docs/r/postgresql_default_privileges.html.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: "postgresql" 3 | page_title: "PostgreSQL: postgresql_default_privileges" 4 | sidebar_current: "docs-postgresql-resource-postgresql_default_privileges" 5 | description: |- 6 | Creates and manages default privileges given to a user for a database schema. 7 | --- 8 | 9 | # postgresql\_default\_privileges 10 | 11 | The ``postgresql_default_privileges`` resource creates and manages default privileges given to a user for a database schema. 12 | 13 | ~> **Note:** This resource needs Postgresql version 9 or above. 14 | 15 | ## Usage 16 | 17 | ```hcl 18 | resource "postgresql_default_privileges" "read_only_tables" { 19 | role = "test_role" 20 | database = "test_db" 21 | schema = "public" 22 | 23 | owner = "db_owner" 24 | object_type = "table" 25 | privileges = ["SELECT"] 26 | } 27 | ``` 28 | 29 | ## Argument Reference 30 | 31 | * `role` - (Required) The role that will automatically be granted the specified privileges on new objects created by the owner. 32 | * `database` - (Required) The database to grant default privileges for this role. 33 | * `owner` - (Required) Specifies the role that creates objects for which the default privileges will be applied. 34 | * `schema` - (Optional) The database schema to set default privileges for this role. 35 | * `object_type` - (Required) The PostgreSQL object type to set the default privileges on (one of: table, sequence, function, type, schema). 36 | * `privileges` - (Required) List of privileges (e.g., SELECT, INSERT, UPDATE, DELETE) to grant on new objects created by the owner. An empty list could be provided to revoke all default privileges for this role. 37 | 38 | 39 | ## Examples 40 | 41 | ### Grant default privileges for tables to "current_role" role: 42 | 43 | ```hcl 44 | resource "postgresql_default_privileges" "grant_table_privileges" { 45 | database = postgresql_database.example_db.name 46 | role = "current_role" 47 | owner = "owner_role" 48 | schema = "public" 49 | object_type = "table" 50 | privileges = ["SELECT", "INSERT", "UPDATE"] 51 | } 52 | ``` 53 | Whenever the `owner_role` creates a new table in the `public` schema, the `current_role` is automatically granted SELECT, INSERT, and UPDATE privileges on that table. 54 | 55 | ### Revoke default privileges for functions for "public" role: 56 | 57 | ```hcl 58 | resource "postgresql_default_privileges" "revoke_public" { 59 | database = postgresql_database.example_db.name 60 | role = "public" 61 | owner = "object_owner" 62 | object_type = "function" 63 | privileges = [] 64 | } 65 | ``` 66 | -------------------------------------------------------------------------------- /website/docs/r/postgresql_extension.html.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: "postgresql" 3 | page_title: "PostgreSQL: postgresql_extension" 4 | sidebar_current: "docs-postgresql-resource-postgresql_extension" 5 | description: |- 6 | Creates and manages an extension on a PostgreSQL server. 7 | --- 8 | 9 | # postgresql\_extension 10 | 11 | The ``postgresql_extension`` resource creates and manages an extension on a PostgreSQL 12 | server. 13 | 14 | 15 | ## Usage 16 | 17 | ```hcl 18 | resource "postgresql_extension" "my_extension" { 19 | name = "pg_trgm" 20 | } 21 | ``` 22 | 23 | ## Argument Reference 24 | 25 | * `name` - (Required) The name of the extension. 26 | * `schema` - (Optional) Sets the schema of an extension. 27 | * `version` - (Optional) Sets the version number of the extension. 28 | * `database` - (Optional) Which database to create the extension on. Defaults to provider database. 29 | * `drop_cascade` - (Optional) When true, will also drop all the objects that depend on the extension, and in turn all objects that depend on those objects. (Default: false) 30 | * `create_cascade` - (Optional) When true, will also create any extensions that this extension depends on that are not already installed. (Default: false) 31 | 32 | ## Import 33 | 34 | PostgreSQL Extensions can be imported using the database name and the extension's resource name, e.g. 35 | 36 | `terraform import postgresql_extension.uuid_ossp example-database.uuid-ossp` 37 | -------------------------------------------------------------------------------- /website/docs/r/postgresql_function.html.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: "postgresql" 3 | page_title: "PostgreSQL: postgresql_function" 4 | sidebar_current: "docs-postgresql-resource-postgresql_function" 5 | description: |- 6 | Creates and manages a function on a PostgreSQL server. 7 | --- 8 | 9 | # postgresql\_function 10 | 11 | The ``postgresql_function`` resource creates and manages a function on a PostgreSQL 12 | server. 13 | 14 | ## Usage 15 | 16 | ```hcl 17 | resource "postgresql_function" "increment" { 18 | name = "increment" 19 | arg { 20 | name = "i" 21 | type = "integer" 22 | } 23 | returns = "integer" 24 | language = "plpgsql" 25 | body = <<-EOF 26 | BEGIN 27 | RETURN i + 1; 28 | END; 29 | EOF 30 | } 31 | ``` 32 | 33 | ## Argument Reference 34 | 35 | * `name` - (Required) The name of the function. 36 | 37 | * `schema` - (Optional) The schema where the function is located. 38 | If not specified, the function is created in the current schema. 39 | 40 | * `database` - (Optional) The database where the function is located. 41 | If not specified, the function is created in the current database. 42 | 43 | * `arg` - (Optional) List of arguments for the function. 44 | * `type` - (Required) The type of the argument. 45 | * `name` - (Optional) The name of the argument. 46 | * `mode` - (Optional) Can be one of IN, INOUT, OUT, or VARIADIC. Default is IN. 47 | * `default` - (Optional) An expression to be used as default value if the parameter is not specified. 48 | 49 | * `returns` - (Optional) Type that the function returns. It can be computed from the OUT arguments. Default is void. 50 | 51 | * `language` - (Optional) The function programming language. Can be one of internal, sql, c, plpgsql. Default is plpgsql. 52 | 53 | * `parallel` - (Optional) Indicates if the function is parallel safe. Can be one of UNSAFE, RESTRICTED, or SAFE. Default is UNSAFE. 54 | 55 | * `security_definer` - (Optional) If the function should execute with the permissions of the owner, rather than the permissions of the caller. Default is false. 56 | 57 | * `strict` - (Optional) If the function should always return NULL when any of the inputs is NULL. Default is false. 58 | 59 | * `volatility` - (Optional) Defines the volatility of the function. Can be one of VOLATILE, STABLE, or IMMUTABLE. Default is VOLATILE. 60 | 61 | * `body` - (Required) Function body. 62 | This should be the body content within the `AS $$` and the final `$$`. It will also accept the `AS $$` and `$$` if added. 63 | 64 | * `drop_cascade` - (Optional) True to automatically drop objects that depend on the function (such as 65 | operators or triggers), and in turn all objects that depend on those objects. Default is false. 66 | 67 | ## Import 68 | 69 | It is possible to import a `postgresql_function` resource with the following 70 | command: 71 | 72 | ``` 73 | $ terraform import postgresql_function.function_foo "my_database.my_schema.my_function_name(arguments)" 74 | ``` 75 | 76 | Where `my_database` is the name of the database containing the schema, 77 | `my_schema` is the name of the schema in the PostgreSQL database, `my_function_name` is the function name to be imported, `arguments` is the argument signature of the function including all non OUT types and 78 | `postgresql_schema.function_foo` is the name of the resource whose state will be 79 | populated as a result of the command. 80 | -------------------------------------------------------------------------------- /website/docs/r/postgresql_grant.html.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: "postgresql" 3 | page_title: "PostgreSQL: postgresql_grant" 4 | sidebar_current: "docs-postgresql-resource-postgresql_grant" 5 | description: |- 6 | Creates and manages privileges given to a user for a database schema. 7 | --- 8 | 9 | # postgresql\_grant 10 | 11 | The ``postgresql_grant`` resource creates and manages privileges given to a user for a database schema. 12 | 13 | See [PostgreSQL documentation](https://www.postgresql.org/docs/current/sql-grant.html) 14 | 15 | ~> **Note:** This resource needs Postgresql version 9 or above. 16 | ~> **Note:** Using column & table grants on the _same_ table with the _same_ privileges can lead to unexpected behaviours. 17 | 18 | ## Usage 19 | 20 | ```hcl 21 | # Grant SELECT privileges on 2 tables 22 | resource "postgresql_grant" "readonly_tables" { 23 | database = "test_db" 24 | role = "test_role" 25 | schema = "public" 26 | object_type = "table" 27 | objects = ["table1", "table2"] 28 | privileges = ["SELECT"] 29 | } 30 | 31 | # Grant SELECT & INSERT privileges on 2 columns in 1 table 32 | resource "postgresql_grant" "read_insert_column" { 33 | database = "test_db" 34 | role = "test_role" 35 | schema = "public" 36 | object_type = "column" 37 | objects = ["table1"] 38 | columns = ["col1", "col2"] 39 | privileges = ["UPDATE", "INSERT"] 40 | } 41 | ``` 42 | 43 | ## Argument Reference 44 | 45 | * `role` - (Required) The name of the role to grant privileges on, Set it to "public" for all roles. 46 | * `database` - (Required) The database to grant privileges on for this role. 47 | * `schema` - The database schema to grant privileges on for this role (Required except if object_type is "database") 48 | * `object_type` - (Required) The PostgreSQL object type to grant the privileges on (one of: database, schema, table, sequence, function, procedure, routine, foreign_data_wrapper, foreign_server, column). 49 | * `privileges` - (Required) The list of privileges to grant. There are different kinds of privileges: SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER, CREATE, CONNECT, TEMPORARY, EXECUTE, and USAGE. An empty list could be provided to revoke all privileges for this role. 50 | * `objects` - (Optional) The objects upon which to grant the privileges. An empty list (the default) means to grant permissions on *all* objects of the specified type. You cannot specify this option if the `object_type` is `database` or `schema`. When `object_type` is `column`, only one value is allowed. 51 | * `columns` - (Optional) The columns upon which to grant the privileges. Required when `object_type` is `column`. You cannot specify this option if the `object_type` is not `column`. 52 | * `with_grant_option` - (Optional) Whether the recipient of these privileges can grant the same privileges to others. Defaults to false. 53 | 54 | 55 | ## Examples 56 | 57 | Revoke default accesses for public schema: 58 | 59 | ```hcl 60 | resource "postgresql_grant" "revoke_public" { 61 | database = "test_db" 62 | role = "public" 63 | schema = "public" 64 | object_type = "schema" 65 | privileges = [] 66 | } 67 | ``` 68 | -------------------------------------------------------------------------------- /website/docs/r/postgresql_grant_role.html.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: "postgresql" 3 | page_title: "PostgreSQL: postgresql_grant_role" 4 | sidebar_current: "docs-postgresql-resource-postgresql_grant_role" 5 | description: |- 6 | Creates and manages membership in a role to one or more other roles. 7 | --- 8 | 9 | # postgresql\_grant\_role 10 | 11 | The ``postgresql_grant_role`` resource creates and manages membership in a role to one or more other roles in a non-authoritative way. 12 | 13 | When using ``postgresql_grant_role`` resource it is likely because the PostgreSQL role you are modifying was created outside of this provider. 14 | 15 | ~> **Note:** This resource needs PostgreSQL version 9 or above. 16 | 17 | ## Usage 18 | 19 | ```hcl 20 | resource "postgresql_grant_role" "grant_root" { 21 | role = "root" 22 | grant_role = "application" 23 | with_admin_option = true 24 | } 25 | ``` 26 | 27 | ~> **Note:** If you use `postgresql_grant_role` for a role that you also manage with a `postgresql_role` resource, you need to ignore the changes of the `roles` attribute in the `postgresql_role` resource or they will fight over what your role grants should be. e.g.: 28 | ```hcl 29 | resource "postgresql_role" "bob" { 30 | name = "bob" 31 | 32 | lifecycle { 33 | ignore_changes = [ 34 | roles, 35 | ] 36 | } 37 | } 38 | 39 | resource "postgresql_grant_role" "bob_admin" { 40 | role = "bob" 41 | grant_role = "admin" 42 | } 43 | ``` 44 | 45 | ## Argument Reference 46 | 47 | * `role` - (Required) The name of the role that is granted a new membership. 48 | * `grant_role` - (Required) The name of the role that is added to `role`. 49 | * `with_admin_option` - (Optional) Giving ability to grant membership to others or not for `role`. (Default: false) 50 | -------------------------------------------------------------------------------- /website/docs/r/postgresql_physical_replication_slot.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: "postgresql" 3 | page_title: "PostgreSQL: postgresql_physical_replication_slot" 4 | sidebar_current: "docs-postgresql-resource-postgresql_physical_replication_slot" 5 | description: |- 6 | Creates and manages a physical replication slot on a PostgreSQL server. 7 | --- 8 | 9 | # postgresql\_physical\_replication\_slot 10 | 11 | The ``postgresql_physical_replication_slot`` resource creates and manages a physical replication slot on a PostgreSQL 12 | server. This is useful to setup a cross datacenter replication, with Patroni for example, or permit 13 | any stand-by cluster to replicate physically data. 14 | 15 | 16 | ## Usage 17 | 18 | ```hcl 19 | resource "postgresql_physical_replication_slot" "my_slot" { 20 | name = "my_slot" 21 | } 22 | ``` 23 | 24 | ## Argument Reference 25 | 26 | * `name` - (Required) The name of the replication slot. 27 | -------------------------------------------------------------------------------- /website/docs/r/postgresql_publication.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: "postgresql" 3 | page_title: "PostgreSQL: postgresql_publication" 4 | sidebar_current: "docs-postgresql-resource-postgresql_publication" 5 | description: |- 6 | Creates and manages a publication in a PostgreSQL server database. 7 | --- 8 | 9 | # postgresql_publication 10 | 11 | The `postgresql_publication` resource creates and manages a publication on a PostgreSQL 12 | server. 13 | 14 | ## Usage 15 | 16 | ```hcl 17 | resource "postgresql_publication" "publication" { 18 | name = "publication" 19 | tables = ["public.test","another_schema.test"] 20 | } 21 | ``` 22 | 23 | ## Argument Reference 24 | 25 | - `name` - (Required) The name of the publication. 26 | - `database` - (Optional) Which database to create the publication on. Defaults to provider database. 27 | - `tables` - (Optional) Which tables add to the publication. By defaults no tables added. Format of table is `.`. If `` is not specified - default database schema will be used. Table string must be listed in alphabetical order. 28 | - `all_tables` - (Optional) Should be ALL TABLES added to the publication. Defaults to 'false' 29 | - `owner` - (Optional) Who owns the publication. Defaults to provider user. 30 | - `drop_cascade` - (Optional) Should all subsequent resources of the publication be dropped. Defaults to 'false' 31 | - `publish_param` - (Optional) Which 'publish' options should be turned on. Default to 'insert','update','delete' 32 | - `publish_via_partition_root_param` - (Optional) Should be option 'publish_via_partition_root' be turned on. Default to 'false' 33 | 34 | ## Import Example 35 | 36 | Publication can be imported using this format: 37 | 38 | ``` 39 | $ terraform import postgresql_publication.publication {{database_name}}.{{publication_name}} 40 | ``` 41 | -------------------------------------------------------------------------------- /website/docs/r/postgresql_replication_slot.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: "postgresql" 3 | page_title: "PostgreSQL: postgresql_replication_slot" 4 | sidebar_current: "docs-postgresql-resource-postgresql_replication_slot" 5 | description: |- 6 | Creates and manages a replication slot on a PostgreSQL server. 7 | --- 8 | 9 | # postgresql\_replication\_slot 10 | 11 | The ``postgresql_replication_slot`` resource creates and manages a replication slot on a PostgreSQL 12 | server. 13 | 14 | 15 | ## Usage 16 | 17 | ```hcl 18 | resource "postgresql_replication_slot" "my_slot" { 19 | name = "my_slot" 20 | plugin = "test_decoding" 21 | } 22 | ``` 23 | 24 | ## Argument Reference 25 | 26 | * `name` - (Required) The name of the replication slot. 27 | * `plugin` - (Required) Sets the output plugin. 28 | * `database` - (Optional) Which database to create the replication slot on. Defaults to provider database. 29 | -------------------------------------------------------------------------------- /website/docs/r/postgresql_role.html.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: "postgresql" 3 | page_title: "PostgreSQL: postgresql_role" 4 | sidebar_current: "docs-postgresql-resource-postgresql_role" 5 | description: |- 6 | Creates and manages a role on a PostgreSQL server. 7 | --- 8 | 9 | # postgresql\_role 10 | 11 | The ``postgresql_role`` resource creates and manages a role on a PostgreSQL 12 | server. 13 | 14 | When a ``postgresql_role`` resource is removed, the PostgreSQL ROLE will 15 | automatically run a [`REASSIGN 16 | OWNED`](https://www.postgresql.org/docs/current/static/sql-reassign-owned.html) 17 | and [`DROP 18 | OWNED`](https://www.postgresql.org/docs/current/static/sql-drop-owned.html) to 19 | the `CURRENT_USER` (normally the connected user for the provider). If the 20 | specified PostgreSQL ROLE owns objects in multiple PostgreSQL databases in the 21 | same PostgreSQL Cluster, one PostgreSQL provider per database must be created 22 | and all but the final ``postgresql_role`` must specify a `skip_drop_role`. 23 | 24 | ~> **Note:** All arguments including role name and password will be stored in the raw state as plain-text. 25 | [Read more about sensitive data in state](https://www.terraform.io/docs/state/sensitive-data.html). 26 | 27 | ## Usage 28 | 29 | ```hcl 30 | resource "postgresql_role" "my_role" { 31 | name = "my_role" 32 | login = true 33 | password = "mypass" 34 | } 35 | 36 | resource "postgresql_role" "my_replication_role" { 37 | name = "replication_role" 38 | replication = true 39 | login = true 40 | connection_limit = 5 41 | password = "md5c98cbfeb6a347a47eb8e96cfb4c4b890" 42 | } 43 | ``` 44 | 45 | ## Argument Reference 46 | 47 | * `name` - (Required) The name of the role. Must be unique on the PostgreSQL 48 | server instance where it is configured. 49 | 50 | * `superuser` - (Optional) Defines whether the role is a "superuser", and 51 | therefore can override all access restrictions within the database. Default 52 | value is `false`. 53 | 54 | * `create_database` - (Optional) Defines a role's ability to execute `CREATE 55 | DATABASE`. Default value is `false`. 56 | 57 | * `create_role` - (Optional) Defines a role's ability to execute `CREATE ROLE`. 58 | A role with this privilege can also alter and drop other roles. Default value 59 | is `false`. 60 | 61 | * `inherit` - (Optional) Defines whether a role "inherits" the privileges of 62 | roles it is a member of. Default value is `true`. 63 | 64 | * `login` - (Optional) Defines whether role is allowed to log in. Roles without 65 | this attribute are useful for managing database privileges, but are not users 66 | in the usual sense of the word. Default value is `false`. 67 | 68 | * `replication` - (Optional) Defines whether a role is allowed to initiate 69 | streaming replication or put the system in and out of backup mode. Default 70 | value is `false` 71 | 72 | * `bypass_row_level_security` - (Optional) Defines whether a role bypasses every 73 | row-level security (RLS) policy. Default value is `false`. 74 | 75 | * `connection_limit` - (Optional) If this role can log in, this specifies how 76 | many concurrent connections the role can establish. `-1` (the default) means no 77 | limit. 78 | 79 | * `encrypted_password` - (Optional) Defines whether the password is stored 80 | encrypted in the system catalogs. Default value is `true`. NOTE: this value 81 | is always set (to the conservative and safe value), but may interfere with the 82 | behavior of 83 | [PostgreSQL's `password_encryption` setting](https://www.postgresql.org/docs/current/static/runtime-config-connection.html#GUC-PASSWORD-ENCRYPTION). 84 | 85 | * `password` - (Optional) Sets the role's password. A password is only of use 86 | for roles having the `login` attribute set to true. 87 | 88 | * `roles` - (Optional) Defines list of roles which will be granted to this new role. 89 | 90 | * `search_path` - (Optional) Alters the search path of this new role. Note that 91 | due to limitations in the implementation, values cannot contain the substring 92 | `", "`. 93 | 94 | * `valid_until` - (Optional) Defines the date and time after which the role's 95 | password is no longer valid. Established connections past this `valid_time` 96 | will have to be manually terminated. This value corresponds to a PostgreSQL 97 | datetime. If omitted or the magic value `NULL` is used, `valid_until` will be 98 | set to `infinity`. Default is `NULL`, therefore `infinity`. 99 | 100 | * `skip_drop_role` - (Optional) When a PostgreSQL ROLE exists in multiple 101 | databases and the ROLE is dropped, the 102 | [cleanup of ownership of objects](https://www.postgresql.org/docs/current/static/role-removal.html) 103 | in each of the respective databases must occur before the ROLE can be dropped 104 | from the catalog. Set this option to true when there are multiple databases 105 | in a PostgreSQL cluster using the same PostgreSQL ROLE for object ownership. 106 | This is the third and final step taken when removing a ROLE from a database. 107 | 108 | * `skip_reassign_owned` - (Optional) When a PostgreSQL ROLE exists in multiple 109 | databases and the ROLE is dropped, a 110 | [`REASSIGN OWNED`](https://www.postgresql.org/docs/current/static/sql-reassign-owned.html) in 111 | must be executed on each of the respective databases before the `DROP ROLE` 112 | can be executed to drop the ROLE from the catalog. This is the first and 113 | second steps taken when removing a ROLE from a database (the second step being 114 | an implicit 115 | [`DROP OWNED`](https://www.postgresql.org/docs/current/static/sql-drop-owned.html)). 116 | 117 | * `statement_timeout` - (Optional) Defines [`statement_timeout`](https://www.postgresql.org/docs/current/runtime-config-client.html#RUNTIME-CONFIG-CLIENT-STATEMENT) setting for this role which allows to abort any statement that takes more than the specified amount of time. 118 | 119 | * `assume_role` - (Optional) Defines the role to switch to at login via [`SET ROLE`](https://www.postgresql.org/docs/current/sql-set-role.html). 120 | 121 | ## Import Example 122 | 123 | `postgresql_role` supports importing resources. Supposing the following 124 | Terraform: 125 | 126 | ```hcl 127 | provider "postgresql" { 128 | alias = "admindb" 129 | } 130 | 131 | resource "postgresql_role" "replication_role" { 132 | provider = "postgresql.admindb" 133 | 134 | name = "replication_name" 135 | } 136 | ``` 137 | 138 | It is possible to import a `postgresql_role` resource with the following 139 | command: 140 | 141 | ``` 142 | $ terraform import postgresql_role.replication_role replication_name 143 | ``` 144 | 145 | Where `replication_name` is the name of the role to import and 146 | `postgresql_role.replication_role` is the name of the resource whose state will 147 | be populated as a result of the command. 148 | -------------------------------------------------------------------------------- /website/docs/r/postgresql_schema.html.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: "postgresql" 3 | page_title: "PostgreSQL: postgresql_schema" 4 | sidebar_current: "docs-postgresql-resource-postgresql_schema" 5 | description: |- 6 | Creates and manages a schema within a PostgreSQL database. 7 | --- 8 | 9 | # postgresql\_schema 10 | 11 | The ``postgresql_schema`` resource creates and manages [schema 12 | objects](https://www.postgresql.org/docs/current/static/ddl-schemas.html) within 13 | a PostgreSQL database. 14 | 15 | 16 | ## Usage 17 | 18 | ```hcl 19 | resource "postgresql_role" "app_www" { 20 | name = "app_www" 21 | } 22 | 23 | resource "postgresql_role" "app_dba" { 24 | name = "app_dba" 25 | } 26 | 27 | resource "postgresql_role" "app_releng" { 28 | name = "app_releng" 29 | } 30 | 31 | resource "postgresql_schema" "my_schema" { 32 | name = "my_schema" 33 | owner = "postgres" 34 | 35 | policy { 36 | usage = true 37 | role = "${postgresql_role.app_www.name}" 38 | } 39 | 40 | # app_releng can create new objects in the schema. This is the role that 41 | # migrations are executed as. 42 | policy { 43 | create = true 44 | usage = true 45 | role = "${postgresql_role.app_releng.name}" 46 | } 47 | 48 | policy { 49 | create_with_grant = true 50 | usage_with_grant = true 51 | role = "${postgresql_role.app_dba.name}" 52 | } 53 | } 54 | ``` 55 | 56 | ## Argument Reference 57 | 58 | * `name` - (Required) The name of the schema. Must be unique in the PostgreSQL 59 | database instance where it is configured. 60 | * `database` - (Optional) The DATABASE in which where this schema will be created. (Default: The database used by your `provider` configuration) 61 | * `owner` - (Optional) The ROLE who owns the schema. 62 | * `if_not_exists` - (Optional) When true, use the existing schema if it exists. (Default: true) 63 | * `drop_cascade` - (Optional) When true, will also drop all the objects that are contained in the schema. (Default: false) 64 | * `policy` - (Optional) Can be specified multiple times for each policy. Each 65 | policy block supports fields documented below. 66 | 67 | The `policy` block supports: 68 | 69 | * `create` - (Optional) Should the specified ROLE have CREATE privileges to the specified SCHEMA. 70 | * `create_with_grant` - (Optional) Should the specified ROLE have CREATE privileges to the specified SCHEMA and the ability to GRANT the CREATE privilege to other ROLEs. 71 | * `role` - (Optional) The ROLE who is receiving the policy. If this value is empty or not specified it implies the policy is referring to the [`PUBLIC` role](https://www.postgresql.org/docs/current/static/sql-grant.html). 72 | * `usage` - (Optional) Should the specified ROLE have USAGE privileges to the specified SCHEMA. 73 | * `usage_with_grant` - (Optional) Should the specified ROLE have USAGE privileges to the specified SCHEMA and the ability to GRANT the USAGE privilege to other ROLEs. 74 | 75 | ~> **NOTE on `policy`:** The permissions of a role specified in multiple policy blocks is cumulative. For example, if the same role is specified in two different `policy` each with different permissions (e.g. `create` and `usage_with_grant`, respectively), then the specified role with have both `create` and `usage_with_grant` privileges. 76 | 77 | ## Import Example 78 | 79 | `postgresql_schema` supports importing resources. Supposing the following 80 | Terraform: 81 | 82 | ```hcl 83 | resource "postgresql_schema" "public" { 84 | name = "public" 85 | } 86 | 87 | resource "postgresql_schema" "schema_foo" { 88 | name = "my_schema" 89 | owner = "postgres" 90 | 91 | policy { 92 | usage = true 93 | } 94 | } 95 | ``` 96 | 97 | It is possible to import a `postgresql_schema` resource with the following 98 | command: 99 | 100 | ``` 101 | $ terraform import postgresql_schema.schema_foo my_database.my_schema 102 | ``` 103 | 104 | Where `my_database` is the name of the database containing the schema, 105 | `my_schema` is the name of the schema in the PostgreSQL database and 106 | `postgresql_schema.schema_foo` is the name of the resource whose state will be 107 | populated as a result of the command. 108 | -------------------------------------------------------------------------------- /website/docs/r/postgresql_security_label.html.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: "postgresql" 3 | page_title: "PostgreSQL: postgresql_grant" 4 | sidebar_current: "docs-postgresql-resource-postgresql_grant" 5 | description: |- 6 | Creates and manages privileges given to a user for a database schema. 7 | --- 8 | 9 | # postgresql\_security\_label 10 | 11 | The ``postgresql_security_label`` resource creates and manages security labels. 12 | 13 | See [PostgreSQL documentation](https://www.postgresql.org/docs/current/sql-security-label.html) 14 | 15 | ~> **Note:** This resource needs Postgresql version 11 or above. 16 | 17 | ## Usage 18 | 19 | ```hcl 20 | resource "postgresql_role" "my_role" { 21 | name = "my_role" 22 | login = true 23 | } 24 | 25 | resource "postgresql_security_label" "workload" { 26 | object_type = "role" 27 | object_name = postgresql_role.my_role.name 28 | label_provider = "pgaadauth" 29 | label = "aadauth,oid=00000000-0000-0000-0000-000000000000,type=service" 30 | } 31 | ``` 32 | 33 | ## Argument Reference 34 | 35 | * `object_type` - (Required) The PostgreSQL object type to apply this security label to. 36 | * `object_name` - (Required) The name of the object to be labeled. Names of objects that reside in schemas (tables, functions, etc.) can be schema-qualified. 37 | * `label_provider` - (Required) The name of the provider with which this label is to be associated. 38 | * `label` - (Required) The value of the security label. 39 | 40 | ## Import 41 | 42 | Security label is an attribute that can be added multiple times, so no import is needed, simply apply again. 43 | -------------------------------------------------------------------------------- /website/docs/r/postgresql_server.html.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: "postgresql" 3 | page_title: "PostgreSQL: postgresql_server" 4 | sidebar_current: "docs-postgresql-resource-postgresql_server" 5 | description: |- 6 | Creates and manages a foreign server on a PostgreSQL server. 7 | --- 8 | 9 | # postgresql\_server 10 | 11 | The ``postgresql_server`` resource creates and manages a foreign server on a PostgreSQL server. 12 | 13 | 14 | ## Usage 15 | 16 | ```hcl 17 | resource "postgresql_extension" "ext_postgres_fdw" { 18 | name = "postgres_fdw" 19 | } 20 | 21 | resource "postgresql_server" "myserver_postgres" { 22 | server_name = "myserver_postgres" 23 | fdw_name = "postgres_fdw" 24 | options = { 25 | host = "foo" 26 | dbname = "foodb" 27 | port = "5432" 28 | } 29 | 30 | depends_on = [postgresql_extension.ext_postgres_fdw] 31 | } 32 | ``` 33 | 34 | ```hcl 35 | resource "postgresql_extension" "ext_file_fdw" { 36 | name = "file_fdw" 37 | } 38 | 39 | resource "postgresql_server" "myserver_file" { 40 | server_name = "myserver_file" 41 | fdw_name = "file_fdw" 42 | depends_on = [postgresql_extension.ext_file_fdw] 43 | } 44 | ``` 45 | 46 | ## Argument Reference 47 | 48 | * `server_name` - (Required) The name of the foreign server to be created. 49 | * `fdw_name` - (Required) The name of the foreign-data wrapper that manages the server. 50 | Changing this value 51 | will force the creation of a new resource as this value can only be set 52 | when the foreign server is created. 53 | * `options` - (Optional) This clause specifies the options for the server. The options typically define the connection details of the server, but the actual names and values are dependent on the server's foreign-data wrapper. 54 | * `server_type` - (Optional) Optional server type, potentially useful to foreign-data wrappers. 55 | Changing this value 56 | will force the creation of a new resource as this value can only be set 57 | when the foreign server is created. 58 | * `server_version` - (Optional) Optional server version, potentially useful to foreign-data wrappers. 59 | * `server_owner` - (Optional) By default, the user who defines the server becomes its owner. Set this value to configure the new owner of the foreign server. 60 | * `drop_cascade` - (Optional) When true, will drop objects that depend on the server (such as user mappings), and in turn all objects that depend on those objects . (Default: false) 61 | -------------------------------------------------------------------------------- /website/docs/r/postgresql_subscription.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: "postgresql" 3 | page_title: "PostgreSQL: postgresql_subscription" 4 | sidebar_current: "docs-postgresql-resource-postgresql_subscription" 5 | description: |- 6 | Creates and manages a subscription in a PostgreSQL server database. 7 | --- 8 | 9 | # postgresql_subscription 10 | 11 | The `postgresql_subscription` resource creates and manages a publication on a PostgreSQL 12 | server. 13 | 14 | ## Usage 15 | 16 | ```hcl 17 | resource "postgresql_subscription" "subscription" { 18 | name = "subscription" 19 | conninfo = "host=localhost port=5432 dbname=mydb user=postgres password=postgres" 20 | publications = ["publication"] 21 | } 22 | ``` 23 | 24 | ## Argument Reference 25 | 26 | - `name` - (Required) The name of the publication. 27 | - `conninfo` - (Required) The connection string to the publisher. It should follow the [keyword/value format](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING) 28 | - `publications` - (Required) Names of the publications on the publisher to subscribe to 29 | - `database` - (Optional) Which database to create the subscription on. Defaults to provider database. 30 | - `create_slot` - (Optional) Specifies whether the command should create the replication slot on the publisher. Default behavior is true 31 | - `slot_name` - (Optional) Name of the replication slot to use. The default behavior is to use the name of the subscription for the slot name 32 | 33 | ## Postgres documentation 34 | - https://www.postgresql.org/docs/current/sql-createsubscription.html -------------------------------------------------------------------------------- /website/docs/r/postgresql_user_mapping.html.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: "postgresql" 3 | page_title: "PostgreSQL: postgresql_user_mapping" 4 | sidebar_current: "docs-postgresql-resource-postgresql_user_mapping" 5 | description: |- 6 | Creates and manages a user mapping on a PostgreSQL server. 7 | --- 8 | 9 | # postgresql\_user\_mapping 10 | 11 | The ``postgresql_user_mapping`` resource creates and manages a user mapping on a PostgreSQL server. 12 | 13 | 14 | ## Usage 15 | 16 | ```hcl 17 | resource "postgresql_extension" "ext_postgres_fdw" { 18 | name = "postgres_fdw" 19 | } 20 | 21 | resource "postgresql_server" "myserver_postgres" { 22 | server_name = "myserver_postgres" 23 | fdw_name = "postgres_fdw" 24 | options = { 25 | host = "foo" 26 | dbname = "foodb" 27 | port = "5432" 28 | } 29 | 30 | depends_on = [postgresql_extension.ext_postgres_fdw] 31 | } 32 | 33 | resource "postgresql_role" "remote" { 34 | name = "remote" 35 | } 36 | 37 | resource "postgresql_user_mapping" "remote" { 38 | server_name = postgresql_server.myserver_postgres.server_name 39 | user_name = postgresql_role.remote.name 40 | options = { 41 | user = "admin" 42 | password = "pass" 43 | } 44 | } 45 | ``` 46 | 47 | ## Argument Reference 48 | 49 | * `user_name` - (Required) The name of an existing user that is mapped to foreign server. CURRENT_ROLE, CURRENT_USER, and USER match the name of the current user. When PUBLIC is specified, a so-called public mapping is created that is used when no user-specific mapping is applicable. 50 | Changing this value 51 | will force the creation of a new resource as this value can only be set 52 | when the user mapping is created. 53 | * `server_name` - (Required) The name of an existing server for which the user mapping is to be created. 54 | Changing this value 55 | will force the creation of a new resource as this value can only be set 56 | when the user mapping is created. 57 | * `options` - (Optional) This clause specifies the options of the user mapping. The options typically define the actual user name and password of the mapping. Option names must be unique. The allowed option names and values are specific to the server's foreign-data wrapper. 58 | -------------------------------------------------------------------------------- /website/postgresql.erb: -------------------------------------------------------------------------------- 1 | <% wrap_layout :inner do %> 2 | <% content_for :sidebar do %> 3 | 78 | <% end %> 79 | 80 | <%= yield %> 81 | <% end %> 82 | --------------------------------------------------------------------------------