├── .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 |
--------------------------------------------------------------------------------