├── documentation ├── .gitkeep ├── partial │ ├── README.md │ ├── _config_file.md │ └── _connection.md ├── README.md ├── postgresql_service.md ├── postgresql_config.md ├── postgresql_ident.md ├── postgresql_extension.md ├── postgresql_access.md ├── postgresql_install.md ├── postgresql_role.md └── postgresql_database.md ├── .gitattributes ├── .github ├── CODEOWNERS ├── lock.yml ├── workflows │ ├── conventional-commits.yml │ ├── prevent-file-change.yml │ ├── copilot-setup-steps.yml │ ├── release.yml │ ├── stale.yml │ └── ci.yml ├── instructions │ └── project.instructions.md └── copilot-instructions.md ├── .mdlrc ├── .release-please-manifest.json ├── .envrc ├── test ├── integration │ ├── client_multi_install │ │ ├── inspec.yml │ │ └── controls │ │ │ └── client_spec.rb │ ├── server_install_os │ │ ├── controls │ │ │ ├── server_spec.rb │ │ │ ├── database.rb │ │ │ └── access.rb │ │ └── inspec.yml │ ├── extension │ │ ├── inspec.yml │ │ └── controls │ │ │ └── extension_spec.rb │ ├── client_install │ │ ├── inspec.yml │ │ └── controls │ │ │ └── client_spec.rb │ ├── access │ │ ├── inspec.yml │ │ └── controls │ │ │ └── base_access.rb │ ├── initdb_locale │ │ ├── inspec.yml │ │ └── controls │ │ │ └── default_spec.rb │ ├── repo │ │ ├── inspec.yml │ │ └── controls │ │ │ └── repo_spec.rb.rb │ ├── ident │ │ ├── inspec.yml │ │ └── controls │ │ │ ├── database.rb │ │ │ └── ident_map.rb │ ├── server_install │ │ ├── inspec.yml │ │ └── controls │ │ │ └── server_spec.rb │ ├── no_repos_install │ │ ├── inspec.yml │ │ ├── no_repos_spec.rb │ │ └── controls │ │ │ └── no_repos_spec.rb │ └── all_repos_install │ │ ├── inspec.yml │ │ └── controls │ │ └── all_repos_spec.rb └── cookbooks │ └── test │ ├── recipes │ ├── repository.rb │ ├── client_install.rb │ ├── multi_client.rb │ ├── initdb_locale.rb │ ├── extension.rb │ ├── server_install.rb │ ├── all_repos_install.rb │ ├── no_repos_install.rb │ ├── ident.rb │ ├── server_install_os.rb │ └── access.rb │ ├── templates │ └── default │ │ └── pg_hba.conf.erb │ └── metadata.rb ├── templates └── default │ ├── systemd │ └── 10-pid.conf.erb │ ├── pg_ident.conf.erb │ ├── postgresql.conf.erb │ ├── pg_hba.conf.erb │ └── createcluster.conf.erb ├── CODE_OF_CONDUCT.md ├── kitchen.exec.yml ├── Berksfile ├── mise.toml ├── .rubocop.yml ├── .vscode └── extensions.json ├── CONTRIBUTING.md ├── .markdownlint-cli2.yaml ├── spec ├── spec_helper.rb └── libraries │ └── helpers_spec.rb ├── .yamllint ├── release-please-config.json ├── .editorconfig ├── renovate.json ├── .windsurf └── rules │ └── definition-of-done.md ├── .overcommit.yml ├── .gitignore ├── metadata.rb ├── kitchen.global.yml ├── libraries ├── sql.rb ├── sql │ ├── attribute.rb │ ├── _utils.rb │ ├── extension.rb │ ├── database.rb │ ├── role.rb │ └── _connection.rb ├── _utils.rb ├── config.rb ├── ident.rb └── helpers.rb ├── resources ├── partial │ ├── _config_file.rb │ └── _connection.rb ├── service.rb ├── extension.rb ├── ident.rb ├── config.rb ├── access.rb ├── database.rb └── role.rb ├── Dangerfile ├── chefignore ├── kitchen.dokken.yml ├── UPGRADING.md ├── TESTING.md ├── README.md ├── kitchen.yml └── LICENSE /documentation/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @sous-chefs/maintainers 2 | -------------------------------------------------------------------------------- /.mdlrc: -------------------------------------------------------------------------------- 1 | rules "~MD007", "~MD013", "~MD024" 2 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "13.0.2" 3 | } 4 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | use chefworkstation 2 | export KITCHEN_GLOBAL_YAML=kitchen.global.yml 3 | -------------------------------------------------------------------------------- /test/integration/client_multi_install/inspec.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: client-multi 3 | -------------------------------------------------------------------------------- /templates/default/systemd/10-pid.conf.erb: -------------------------------------------------------------------------------- 1 | [Service] 2 | PIDFile=/var/run/postgresql/<%=@version%>-main.pid 3 | -------------------------------------------------------------------------------- /test/cookbooks/test/recipes/repository.rb: -------------------------------------------------------------------------------- 1 | postgresql_install 'postgresql' do 2 | action :repository 3 | end 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Community Guidelines 2 | 3 | This project follows the Chef Community Guidelines 4 | -------------------------------------------------------------------------------- /kitchen.exec.yml: -------------------------------------------------------------------------------- 1 | --- 2 | driver: { name: exec } 3 | transport: { name: exec } 4 | 5 | platforms: 6 | - name: macos-latest 7 | - name: windows-latest 8 | -------------------------------------------------------------------------------- /Berksfile: -------------------------------------------------------------------------------- 1 | source 'https://supermarket.chef.io' 2 | 3 | metadata 4 | 5 | group :integration do 6 | cookbook 'test', path: './test/cookbooks/test' 7 | end 8 | -------------------------------------------------------------------------------- /test/cookbooks/test/recipes/client_install.rb: -------------------------------------------------------------------------------- 1 | postgresql_install 'postgresql' do 2 | version node['test']['pg_ver'] 3 | action %i(install_client) 4 | end 5 | -------------------------------------------------------------------------------- /mise.toml: -------------------------------------------------------------------------------- 1 | # .mise.toml 2 | 3 | [env] 4 | PATH = "/opt/chef-workstation/bin:/opt/chef-workstation/embedded/bin:{{env.PATH}}" 5 | KITCHEN_LOCAL_YAML = "kitchen.dokken.yml" 6 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | require: 2 | - cookstyle 3 | 4 | AllCops: 5 | TargetRubyVersion: 3.1 6 | Include: 7 | - "**/*.rb" 8 | Exclude: 9 | - "vendor/**/*" 10 | - "spec/**/*" 11 | -------------------------------------------------------------------------------- /documentation/partial/README.md: -------------------------------------------------------------------------------- 1 | # Index 2 | 3 | [Back to resource list](../README.md#resources) 4 | 5 | ## Resources 6 | 7 | - [_config_file](_config_file.md) 8 | - [_connection](_connection.md) 9 | -------------------------------------------------------------------------------- /test/integration/server_install_os/controls/server_spec.rb: -------------------------------------------------------------------------------- 1 | describe service('postgresql') do 2 | it { should be_installed } 3 | it { should be_enabled } 4 | it { should be_running } 5 | end 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "chef-software.chef", 4 | "Shopify.ruby-lsp", 5 | "editorconfig.editorconfig", 6 | "DavidAnson.vscode-markdownlint" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /templates/default/pg_ident.conf.erb: -------------------------------------------------------------------------------- 1 | # 2 | # Generated by Chef for <%= node['fqdn'] %> 3 | # Do NOT modify this file by hand. 4 | # 5 | 6 | # MAPNAME SYSTEM-USERNAME PG-USERNAME 7 | <%= @pg_ident.to_s %> 8 | -------------------------------------------------------------------------------- /templates/default/postgresql.conf.erb: -------------------------------------------------------------------------------- 1 | # 2 | # Generated by Chef for <%= node['fqdn'] %> 3 | # Do NOT modify this file by hand. 4 | # 5 | 6 | <%= PostgreSQL::Cookbook::ConfigHelpers.postgresql_conf_file_string(@config) %> 7 | -------------------------------------------------------------------------------- /test/cookbooks/test/templates/default/pg_hba.conf.erb: -------------------------------------------------------------------------------- 1 | # This file was automatically generated and dropped off by Chef! 2 | 3 | ########### 4 | # From the postgresql_access resources 5 | ########### 6 | <%= @pg_hba.to_s %> 7 | -------------------------------------------------------------------------------- /test/cookbooks/test/recipes/multi_client.rb: -------------------------------------------------------------------------------- 1 | postgresql_install '16' do 2 | version '16' 3 | 4 | action :install_client 5 | end 6 | 7 | postgresql_install '15' do 8 | version '15' 9 | 10 | action :install_client 11 | end 12 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Please refer to 4 | [https://github.com/chef-cookbooks/community_cookbook_documentation/blob/main/CONTRIBUTING.MD](https://github.com/chef-cookbooks/community_cookbook_documentation/blob/main/CONTRIBUTING.MD) 5 | -------------------------------------------------------------------------------- /.github/lock.yml: -------------------------------------------------------------------------------- 1 | --- 2 | daysUntilLock: 365 3 | exemptLabels: [] 4 | lockLabel: false 5 | lockComment: > 6 | This thread has been automatically locked since there has not been 7 | any recent activity after it was closed. Please open a new issue for 8 | related bugs. 9 | -------------------------------------------------------------------------------- /test/cookbooks/test/metadata.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | name 'test' 3 | maintainer 'Sous Chefs' 4 | maintainer_email 'help@sous-chefs.org' 5 | license 'Apache-2.0' 6 | description 'Installs/Configures test' 7 | version '0.1.0' 8 | 9 | depends 'postgresql' 10 | -------------------------------------------------------------------------------- /templates/default/pg_hba.conf.erb: -------------------------------------------------------------------------------- 1 | # 2 | # Generated by Chef for <%= node['fqdn'] %> 3 | # Do NOT modify this file by hand. 4 | # 5 | 6 | # TYPE DATABASE USER ADDRESS METHOD OPTIONS 7 | <%= @pg_hba.to_s %> 8 | -------------------------------------------------------------------------------- /test/integration/extension/inspec.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: extension 3 | title: PostgreSQL extension tests 4 | maintainer: Sous Chefs 5 | copyright_email: help@sous-chefs.org 6 | license: Apache 7 | summary: Check that extensions are installed 8 | version: 0.0.1 9 | supports: 10 | - os-family: unix 11 | -------------------------------------------------------------------------------- /.markdownlint-cli2.yaml: -------------------------------------------------------------------------------- 1 | config: 2 | ul-indent: false # MD007 3 | line-length: false # MD013 4 | no-duplicate-heading: false # MD024 5 | reference-links-images: false # MD052 6 | no-multiple-blanks: 7 | maximum: 2 8 | ignores: 9 | - .github/copilot-instructions.md 10 | - .windsurf/** 11 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'chefspec' 2 | require 'chefspec/berkshelf' 3 | 4 | RSpec.configure do |config| 5 | config.color = true # Use color in STDOUT 6 | config.formatter = :documentation # Use the specified formatter 7 | config.log_level = :error # Avoid deprecation notice SPAM 8 | end 9 | -------------------------------------------------------------------------------- /test/integration/client_install/inspec.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: client 3 | title: PostgreSQL client integration tests 4 | maintainer: Sous Chefs 5 | copyright_email: help@sous-chefs.org 6 | license: Apache 7 | summary: Check that the client is installed and working 8 | version: 0.0.1 9 | supports: 10 | - os-family: unix 11 | -------------------------------------------------------------------------------- /test/integration/access/inspec.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: access 3 | title: PostgreSQL HBA Access integration tests 4 | maintainer: Aaron Kalin 5 | copyright_email: aaron.kalin@dnsimple.com 6 | license: Apache 7 | summary: Check that proper access is provided in the pg_hba conf 8 | version: 0.0.1 9 | supports: 10 | - os-family: unix 11 | -------------------------------------------------------------------------------- /test/integration/initdb_locale/inspec.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: initdb_locale 3 | title: PostgreSQL server 4 | maintainer: Sous Chefs 5 | copyright_email: help@sous-chefs.org 6 | license: Apache 7 | summary: Verify the locale is set correctly when we pass in a non-default locale 8 | version: 0.0.1 9 | supports: 10 | - os-family: unix 11 | -------------------------------------------------------------------------------- /test/integration/repo/inspec.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: client 3 | title: PostgreSQL repository integration tests 4 | maintainer: Dan Webb 5 | copyright_email: dan.webb@damacus.io 6 | license: Apache 7 | summary: Check that repostories are enabled and we can download a package 8 | version: 0.0.1 9 | supports: 10 | - os-family: unix 11 | -------------------------------------------------------------------------------- /test/integration/ident/inspec.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: ident 3 | title: PostgreSQL Ident Map integration tests 4 | maintainer: Aaron Kalin 5 | copyright_email: aaron.kalin@dnsimple.com 6 | license: Apache 7 | summary: Check that proper access maps are provided in the pg_ident conf 8 | version: 0.0.1 9 | supports: 10 | - os-family: unix 11 | -------------------------------------------------------------------------------- /.github/workflows/conventional-commits.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: conventional-commits 3 | 4 | "on": 5 | pull_request: 6 | types: 7 | - opened 8 | - reopened 9 | - edited 10 | - synchronize 11 | 12 | jobs: 13 | conventional-commits: 14 | uses: sous-chefs/.github/.github/workflows/conventional-commits.yml@5.0.8 15 | -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | --- 2 | extends: default 3 | rules: 4 | line-length: 5 | max: 256 6 | level: warning 7 | document-start: disable 8 | braces: 9 | forbid: false 10 | min-spaces-inside: 0 11 | max-spaces-inside: 1 12 | min-spaces-inside-empty: -1 13 | max-spaces-inside-empty: -1 14 | comments: 15 | min-spaces-from-content: 1 16 | -------------------------------------------------------------------------------- /test/integration/server_install/inspec.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: server 3 | title: PostgreSQL server 4 | maintainer: Sous Chefs 5 | copyright_email: help@sous-chefs.org 6 | license: Apache 7 | summary: Verify the correct installation of the postgresql server 8 | version: 0.0.1 9 | supports: 10 | - os-family: unix 11 | depends: 12 | - name: client 13 | path: ./test/integration/client_install 14 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": { 3 | ".": { 4 | "package-name": "postgresql", 5 | "changelog-path": "CHANGELOG.md", 6 | "release-type": "ruby", 7 | "include-component-in-tag": false, 8 | "version-file": "metadata.rb" 9 | } 10 | }, 11 | "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json" 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/prevent-file-change.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: prevent-file-change 3 | 4 | "on": 5 | pull_request: 6 | types: 7 | - opened 8 | - reopened 9 | - edited 10 | - synchronize 11 | 12 | jobs: 13 | prevent-file-change: 14 | uses: sous-chefs/.github/.github/workflows/prevent-file-change.yml@5.0.8 15 | secrets: 16 | token: ${{ secrets.GITHUB_TOKEN }} 17 | -------------------------------------------------------------------------------- /test/integration/server_install_os/inspec.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: server 3 | title: PostgreSQL server 4 | maintainer: Sous Chefs 5 | copyright_email: help@sous-chefs.org 6 | license: Apache 7 | summary: Verify the correct installation of the postgresql server 8 | version: 0.0.1 9 | supports: 10 | - os-family: unix 11 | # depends: 12 | # - name: client 13 | # path: ./test/integration/client_install 14 | -------------------------------------------------------------------------------- /test/integration/client_install/controls/client_spec.rb: -------------------------------------------------------------------------------- 1 | pg_ver = input('pg_ver') 2 | 3 | control 'postgresql-client-install' do 4 | impact 1.0 5 | desc 'These tests ensure a postgresql client installed correctly' 6 | 7 | describe command('/usr/bin/psql -V') do 8 | its('stdout') { should match(/psql \(PostgreSQL\) #{pg_ver}.\d/) } 9 | its('exit_status') { should eq 0 } 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/integration/ident/controls/database.rb: -------------------------------------------------------------------------------- 1 | 2 | control 'test1 database should exist' do 3 | impact 1.0 4 | desc 'The test1 database should exist' 5 | 6 | postgres_access = postgres_session('postgres', '12345', '127.0.0.1') 7 | 8 | describe postgres_access.query('SELECT * FROM pg_database;') do 9 | its('output') { should include 'test1' } 10 | its('output') { should_not include 'test2' } 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root=true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | # 2 space indentation 12 | indent_style = space 13 | indent_size = 2 14 | 15 | # Avoid issues parsing cookbook files later 16 | charset = utf-8 17 | 18 | # Avoid cookstyle warnings 19 | trim_trailing_whitespace = true 20 | -------------------------------------------------------------------------------- /test/integration/no_repos_install/inspec.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: no_repos_install 3 | title: PostgreSQL no_repos_install 4 | maintainer: Sous Chefs 5 | copyright_email: help@sous-chefs.org 6 | license: Apache 7 | summary: Verify the correct installation of the postgresql server with cookbook provided yum repos instead of resource 8 | version: 0.0.1 9 | supports: 10 | - os-family: unix 11 | depends: 12 | - name: client 13 | path: ./test/integration/client_install 14 | -------------------------------------------------------------------------------- /test/integration/server_install_os/controls/database.rb: -------------------------------------------------------------------------------- 1 | 2 | control 'test1 database should exist' do 3 | impact 1.0 4 | desc 'The test1 database should exist' 5 | 6 | postgres_access = postgres_session('postgres', '12345', '127.0.0.1') 7 | 8 | describe postgres_access.query('SELECT * FROM pg_database;') do 9 | its('output') { should include 'sous_chef' } 10 | its('output') { should include 'test1' } 11 | its('output') { should_not include 'test2' } 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/integration/all_repos_install/inspec.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: all_repos_install 3 | title: PostgreSQL server with all repos setup but only pgdg and pgdg-common enabled 4 | maintainer: Sous Chefs 5 | copyright_email: help@sous-chefs.org 6 | license: Apache 7 | summary: Verify the correct installation of the postgresql server including correct repos setup and enabled 8 | version: 0.0.1 9 | supports: 10 | - os-family: unix 11 | depends: 12 | - name: client 13 | path: ./test/integration/client_install 14 | -------------------------------------------------------------------------------- /documentation/README.md: -------------------------------------------------------------------------------- 1 | # Index 2 | 3 | [Back to resource list](../README.md#resources) 4 | 5 | ## Resources 6 | 7 | - [postgresql_access](postgresql_access.md) 8 | - [postgresql_config](postgresql_config.md) 9 | - [postgresql_database](postgresql_database.md) 10 | - [postgresql_extension](postgresql_extension.md) 11 | - [postgresql_ident](postgresql_ident.md) 12 | - [postgresql_install](postgresql_install.md) 13 | - [postgresql_role](postgresql_role.md) 14 | - [postgresql_service](postgresql_service.md) 15 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:base"], 4 | "packageRules": [ 5 | { 6 | "groupName": "Actions", 7 | "matchUpdateTypes": ["minor", "patch", "pin"], 8 | "automerge": true, 9 | "addLabels": ["Release: Patch", "Skip: Announcements"] 10 | }, 11 | { 12 | "groupName": "Actions", 13 | "matchUpdateTypes": ["major"], 14 | "automerge": false, 15 | "addLabels": ["Release: Patch", "Skip: Announcements"] 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.windsurf/rules/definition-of-done.md: -------------------------------------------------------------------------------- 1 | --- 2 | trigger: model_decision 3 | description: When completing a task check the following are true 4 | --- 5 | 6 | - `cookstyle` does not return any syntax or stlye errors 7 | - markdownlint-cli2 "**/*.md" "!vendor" "!.venv" --fix 8 | - yamllint 9 | - `kitchen test` does not return any errors 10 | - run all suites 11 | - do not skip suites 12 | This gives us knowledge that we have not broken areas of the cookbook we are not currently changing (regression) 13 | No matter what we have done, even if you think it is outside our control, kitchen test must pass 14 | -------------------------------------------------------------------------------- /.overcommit.yml: -------------------------------------------------------------------------------- 1 | --- 2 | PreCommit: 3 | TrailingWhitespace: 4 | enabled: true 5 | YamlLint: 6 | enabled: true 7 | required_executable: "yamllint" 8 | ChefSpec: 9 | enabled: true 10 | required_executable: "chef" 11 | command: ["chef", "exec", "rspec"] 12 | Cookstyle: 13 | enabled: true 14 | required_executable: "cookstyle" 15 | command: ["cookstyle"] 16 | MarkdownLint: 17 | enabled: false 18 | required_executable: "npx" 19 | command: ["npx", "markdownlint-cli2", "'**/*.md'"] 20 | include: ["**/*.md"] 21 | 22 | CommitMsg: 23 | HardTabs: 24 | enabled: true 25 | -------------------------------------------------------------------------------- /.github/workflows/copilot-setup-steps.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'Copilot Setup Steps' 3 | 4 | "on": 5 | workflow_dispatch: 6 | push: 7 | paths: 8 | - .github/workflows/copilot-setup-steps.yml 9 | pull_request: 10 | paths: 11 | - .github/workflows/copilot-setup-steps.yml 12 | 13 | jobs: 14 | copilot-setup-steps: 15 | runs-on: ubuntu-latest 16 | permissions: 17 | contents: read 18 | steps: 19 | - name: Check out code 20 | uses: actions/checkout@v5 21 | - name: Install Chef 22 | uses: actionshub/chef-install@main 23 | - name: Install cookbooks 24 | run: berks install 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.rbc 2 | .config 3 | InstalledFiles 4 | pkg 5 | test/tmp 6 | test/version_tmp 7 | tmp 8 | _Store 9 | *~ 10 | *# 11 | .#* 12 | \#*# 13 | *.un~ 14 | *.tmp 15 | *.bk 16 | *.bkup 17 | 18 | # editor files 19 | .idea 20 | .*.sw[a-z] 21 | 22 | # ruby/bundler/rspec files 23 | .ruby-version 24 | .ruby-gemset 25 | .rvmrc 26 | Gemfile.lock 27 | .bundle 28 | *.gem 29 | coverage 30 | spec/reports 31 | 32 | # YARD / rdoc artifacts 33 | .yardoc 34 | _yardoc 35 | doc/ 36 | rdoc 37 | 38 | # chef infra stuff 39 | Berksfile.lock 40 | .kitchen 41 | kitchen.local.yml 42 | vendor/ 43 | .coverage/ 44 | .zero-knife.rb 45 | Policyfile.lock.json 46 | 47 | # vagrant stuff 48 | .vagrant/ 49 | .vagrant.d/ 50 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: release 3 | 4 | 5 | "on": 6 | push: 7 | branches: [main] 8 | 9 | permissions: 10 | contents: write 11 | issues: write 12 | pull-requests: write 13 | packages: write 14 | attestations: write 15 | id-token: write 16 | 17 | jobs: 18 | release: 19 | uses: sous-chefs/.github/.github/workflows/release-cookbook.yml@main 20 | secrets: 21 | token: ${{ secrets.PORTER_GITHUB_TOKEN }} 22 | supermarket_user: ${{ secrets.CHEF_SUPERMARKET_USER }} 23 | supermarket_key: ${{ secrets.CHEF_SUPERMARKET_KEY }} 24 | slack_bot_token: ${{ secrets.SLACK_BOT_TOKEN }} 25 | slack_channel_id: ${{ secrets.SLACK_CHANNEL_ID }} 26 | -------------------------------------------------------------------------------- /metadata.rb: -------------------------------------------------------------------------------- 1 | name 'postgresql' 2 | maintainer 'Sous Chefs' 3 | maintainer_email 'help@sous-chefs.org' 4 | license 'Apache-2.0' 5 | description 'Installs and configures postgresql for clients or servers' 6 | version '13.0.2' 7 | source_url 'https://github.com/sous-chefs/postgresql' 8 | issues_url 'https://github.com/sous-chefs/postgresql/issues' 9 | chef_version '>= 18.0' 10 | 11 | depends 'yum', '>= 7.2' 12 | 13 | gem 'deepsort', '~> 0.5.0' 14 | gem 'inifile', '~> 3.0' 15 | 16 | supports 'amazon' 17 | supports 'centos' 18 | supports 'debian' 19 | supports 'oracle' 20 | supports 'redhat' 21 | supports 'scientific' 22 | supports 'ubuntu' 23 | -------------------------------------------------------------------------------- /test/cookbooks/test/recipes/initdb_locale.rb: -------------------------------------------------------------------------------- 1 | postgresql_install 'postgresql' do 2 | version node['test']['pg_ver'] 3 | initdb_locale node['platform_version'].to_i < 8 ? 'en_US.utf8' : 'C.UTF-8' 4 | 5 | action %i(install init_server) 6 | end 7 | 8 | postgresql_access 'local all all peer delete' do 9 | type 'local' 10 | database 'all' 11 | user 'all' 12 | auth_method 'peer' 13 | 14 | action :delete 15 | end 16 | 17 | postgresql_service 'postgresql' do 18 | action %i(enable start) 19 | end 20 | 21 | postgresql_user 'postgres' do 22 | unencrypted_password '12345' 23 | action :set_password 24 | not_if { ::File.exist?('/tmp/postgres-user.txt') } 25 | end 26 | 27 | file '/tmp/postgres-user.txt' 28 | -------------------------------------------------------------------------------- /test/integration/extension/controls/extension_spec.rb: -------------------------------------------------------------------------------- 1 | control 'postgresql-extension' do 2 | impact 1.0 3 | desc 'Check if the "plpgsql" extension was installed successfully' 4 | 5 | postgres_access = postgres_session('postgres', '12345', '127.0.0.1') 6 | 7 | describe postgres_access.query('\dx;', ['test_1']) do 8 | its('output') { should include 'plpgsql' } 9 | end 10 | end 11 | 12 | control 'postgresql-extension-hyphenated' do 13 | impact 1.0 14 | desc 'Check if the "uuid-ossp" extension was installed successfully' 15 | 16 | postgres_access = postgres_session('postgres', '12345', '127.0.0.1') 17 | 18 | describe postgres_access.query('\dx;', ['test_1']) do 19 | its('output') { should include 'ossp' } 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /documentation/postgresql_service.md: -------------------------------------------------------------------------------- 1 | # postgresql_service 2 | 3 | [Back to resource list](../README.md#resources) 4 | 5 | ## Actions 6 | 7 | - `:start` 8 | - `:stop` 9 | - `:restart` 10 | - `:reload` 11 | - `:enable` 12 | - `:disable` 13 | 14 | ## Properties 15 | 16 | | Name | Name? | Type | Default | Description | Allowed Values | 17 | | -------------- | ----- | ----------- | ------- | ------------------------------------ | -------------- | 18 | | `service_name` | | String | | Service name to perform actions for | | 19 | | `delay_start` | | true, false | | Delay service start until end of run | | 20 | 21 | ## Libraries 22 | 23 | - `PostgreSQL::Cookbook::Helpers` 24 | -------------------------------------------------------------------------------- /test/integration/client_multi_install/controls/client_spec.rb: -------------------------------------------------------------------------------- 1 | pg_path = 2 | case os.family 3 | when 'debian' 4 | '/usr/lib/postgresql/' 5 | when 'redhat' 6 | '/usr/pgsql-' 7 | end 8 | 9 | control 'postgresql-client-multi-install' do 10 | describe command('/usr/bin/psql -V') do 11 | its('stdout') { should match(/psql \(PostgreSQL\) 12.\d/) } 12 | its('exit_status') { should eq 0 } 13 | end 14 | 15 | describe command("#{pg_path}11/bin/psql -V") do 16 | its('stdout') { should match(/psql \(PostgreSQL\) 11.\d/) } 17 | its('exit_status') { should eq 0 } 18 | end 19 | 20 | describe command("#{pg_path}12/bin/psql -V") do 21 | its('stdout') { should match(/psql \(PostgreSQL\) 12.\d/) } 22 | its('exit_status') { should eq 0 } 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/integration/repo/controls/repo_spec.rb.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | pg_ver = input('pg_ver') 3 | 4 | case os[:family] 5 | 6 | when 'redhat' 7 | describe yum.repo("pgdg#{pg_ver}") do 8 | it { should exist } 9 | it { should be_enabled } 10 | end 11 | 12 | describe yum.repo("pgdg#{pg_ver}-source") do 13 | it { should exist } 14 | it { should_not be_enabled } 15 | end 16 | 17 | describe yum.repo("pgdg#{pg_ver}-source-updates-testing") do 18 | it { should exist } 19 | it { should_not be_enabled } 20 | end 21 | 22 | describe yum.repo("pgdg#{pg_ver}-updates-testing") do 23 | it { should exist } 24 | it { should_not be_enabled } 25 | end 26 | 27 | when 'debian' 28 | describe apt('https://download.postgresql.org/pub/repos/apt/') do 29 | it { should exist } 30 | it { should be_enabled } 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /documentation/partial/_config_file.md: -------------------------------------------------------------------------------- 1 | # _config_file 2 | 3 | [Back to resource list](../README.md#resources) 4 | 5 | ## Actions 6 | 7 | - None 8 | 9 | ## Properties 10 | 11 | | Name | Name? | Type | Default | Description | Allowed Values | 12 | | ------------- | ----- | ------ | ------- | ----------- | -------------- | 13 | | `config_file` | | String | | | | 14 | | `cookbook` | | String | | | | 15 | | `source` | | String | | | | 16 | | `owner` | | String | | | | 17 | | `group` | | String | | | | 18 | | `filemode` | | String | | | | 19 | 20 | ## Libraries 21 | 22 | - `PostgreSQL::Cookbook::Helpers` 23 | -------------------------------------------------------------------------------- /test/integration/no_repos_install/no_repos_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | pg_ver = input('pg_ver') 3 | 4 | if os[:family] == 'redhat' 5 | describe service("postgresql-#{pg_ver}") do 6 | it { should be_installed } 7 | it { should be_enabled } 8 | it { should be_running } 9 | end 10 | %W(pgdg#{pg_ver} pgdg-common).each do |r| 11 | describe yum.repo(r) do 12 | it { should exist } 13 | it { should be_enabled } 14 | end 15 | end 16 | %W(pgdg#{pg_ver}-source pgdg#{pg_ver}-updates-testing pgdg#{pg_ver}-source-updates-testing).each do |r| 17 | describe yum.repo(r) do 18 | it { should exist } 19 | it { should_not be_enabled } 20 | end 21 | end 22 | else 23 | describe service('postgresql') do 24 | it { should be_installed } 25 | it { should be_enabled } 26 | it { should be_running } 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/integration/all_repos_install/controls/all_repos_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | pg_ver = input('pg_ver') 3 | 4 | if os[:family] == 'redhat' 5 | describe service("postgresql-#{pg_ver}") do 6 | it { should be_installed } 7 | it { should be_enabled } 8 | it { should be_running } 9 | end 10 | %W(pgdg#{pg_ver} pgdg-common).each do |r| 11 | describe yum.repo(r) do 12 | it { should exist } 13 | it { should be_enabled } 14 | end 15 | end 16 | %W(pgdg#{pg_ver}-source pgdg#{pg_ver}-updates-testing pgdg#{pg_ver}-source-updates-testing).each do |r| 17 | describe yum.repo(r) do 18 | it { should exist } 19 | it { should_not be_enabled } 20 | end 21 | end 22 | else 23 | describe service('postgresql') do 24 | it { should be_installed } 25 | it { should be_enabled } 26 | it { should be_running } 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/integration/server_install/controls/server_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | pg_ver = input('pg_ver') 3 | 4 | if os[:family] == 'redhat' 5 | describe service("postgresql-#{pg_ver}") do 6 | it { should be_installed } 7 | it { should be_enabled } 8 | it { should be_running } 9 | end 10 | %W(pgdg#{pg_ver} pgdg-common).each do |r| 11 | describe yum.repo(r) do 12 | it { should exist } 13 | it { should be_enabled } 14 | end 15 | end 16 | %W(pgdg#{pg_ver}-source pgdg#{pg_ver}-updates-testing pgdg#{pg_ver}-source-updates-testing).each do |r| 17 | describe yum.repo(r) do 18 | it { should_not exist } 19 | it { should_not be_enabled } 20 | end 21 | end 22 | else 23 | describe service('postgresql') do 24 | it { should be_installed } 25 | it { should be_enabled } 26 | it { should be_running } 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/integration/no_repos_install/controls/no_repos_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | pg_ver = input('pg_ver') 3 | 4 | if os[:family] == 'redhat' 5 | describe service("postgresql-#{pg_ver}") do 6 | it { should be_installed } 7 | it { should be_enabled } 8 | it { should be_running } 9 | end 10 | %W(pgdg#{pg_ver} pgdg-common).each do |r| 11 | describe yum.repo(r) do 12 | it { should_not exist } 13 | it { should_not be_enabled } 14 | end 15 | end 16 | %W(pgdg#{pg_ver}-source pgdg#{pg_ver}-updates-testing pgdg#{pg_ver}-source-updates-testing).each do |r| 17 | describe yum.repo(r) do 18 | it { should_not exist } 19 | it { should_not be_enabled } 20 | end 21 | end 22 | else 23 | describe service('postgresql') do 24 | it { should be_installed } 25 | it { should be_enabled } 26 | it { should be_running } 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /kitchen.global.yml: -------------------------------------------------------------------------------- 1 | --- 2 | provisioner: 3 | name: chef_infra 4 | product_name: chef 5 | product_version: <%= ENV['CHEF_VERSION'] || 'latest' %> 6 | channel: stable 7 | install_strategy: once 8 | chef_license: accept 9 | enforce_idempotency: <%= ENV['ENFORCE_IDEMPOTENCY'] || true %> 10 | multiple_converge: <%= ENV['MULTIPLE_CONVERGE'] || 2 %> 11 | deprecations_as_errors: true 12 | log_level: <%= ENV['CHEF_LOG_LEVEL'] || 'auto' %> 13 | 14 | verifier: 15 | name: inspec 16 | 17 | platforms: 18 | - name: almalinux-9 19 | - name: amazonlinux-2023 20 | - name: centos-stream-9 21 | - name: centos-stream-10 22 | - name: debian-12 23 | - name: debian-13 24 | - name: fedora-latest 25 | - name: opensuse-leap-15 26 | - name: oraclelinux-8 27 | - name: oraclelinux-9 28 | - name: rockylinux-8 29 | - name: rockylinux-9 30 | - name: ubuntu-22.04 31 | - name: ubuntu-24.04 32 | -------------------------------------------------------------------------------- /test/integration/ident/controls/ident_map.rb: -------------------------------------------------------------------------------- 1 | control 'postgresql-ident-map' do 2 | impact 1.0 3 | desc 'This test ensures postgres configures ident access correctly' 4 | 5 | # Use su instead of sudo - we're already root in the container and su doesn't require PAM auth 6 | describe command("su - shef -c \"psql -U sous_chef -d postgres -c 'SELECT 1;'\"") do 7 | its('exit_status') { should eq 0 } 8 | its('stderr') { should eq '' } 9 | its('stdout') { should match(/1/) } 10 | end 11 | end 12 | 13 | control 'shef and postgres roles should exist' do 14 | impact 1.0 15 | desc 'The shef & postgres database user role should exist' 16 | 17 | postgres_access = postgres_session('postgres', '12345', '127.0.0.1') 18 | 19 | describe postgres_access.query('SELECT rolname FROM pg_roles;') do 20 | its('output') { should include 'postgres' } 21 | its('output') { should include 'sous_chef' } 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/integration/initdb_locale/controls/default_spec.rb: -------------------------------------------------------------------------------- 1 | control 'postgresql-initdb-locale' do 2 | impact 1.0 3 | desc 'This test ensures the locales are correcly set' 4 | 5 | postgres_access = postgres_session('postgres', '12345', '127.0.0.1') 6 | 7 | describe postgres_access.query('SHOW LC_MONETARY;') do 8 | its('output') { should include (os.release.to_i < 8 ? 'en_US.utf8' : 'C.UTF-8').to_s } 9 | end 10 | 11 | describe postgres_access.query('SHOW LC_MESSAGES;') do 12 | its('output') { should include (os.release.to_i < 8 ? 'en_US.utf8' : 'C.UTF-8').to_s } 13 | end 14 | 15 | describe postgres_access.query('SHOW LC_NUMERIC;') do 16 | its('output') { should include (os.release.to_i < 8 ? 'en_US.utf8' : 'C.UTF-8').to_s } 17 | end 18 | 19 | describe postgres_access.query('SHOW LC_TIME;') do 20 | its('output') { should include (os.release.to_i < 8 ? 'en_US.utf8' : 'C.UTF-8').to_s } 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Mark stale issues and pull requests 3 | 4 | "on": 5 | schedule: [cron: "0 0 * * *"] 6 | 7 | jobs: 8 | stale: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/stale@v10 12 | with: 13 | repo-token: ${{ secrets.GITHUB_TOKEN }} 14 | close-issue-message: > 15 | Closing due to inactivity. 16 | If this is still an issue please reopen or open another issue. 17 | Alternatively drop by the #sous-chefs channel on the [Chef Community Slack](http://community-slack.chef.io/) and we'll be happy to help! 18 | Thanks, Sous-Chefs. 19 | days-before-close: 7 20 | days-before-stale: 365 21 | stale-issue-message: > 22 | Marking stale due to inactivity. 23 | Remove stale label or comment or this will be closed in 7 days. 24 | Alternatively drop by the #sous-chefs channel on the [Chef Community Slack](http://community-slack.chef.io/) and we'll be happy to help! 25 | Thanks, Sous-Chefs. 26 | -------------------------------------------------------------------------------- /libraries/sql.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Cookbook:: postgresql 3 | # Library:: sql 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | require_relative 'sql/_connection' 19 | require_relative 'sql/_utils' 20 | require_relative 'sql/attribute' 21 | require_relative 'sql/database' 22 | require_relative 'sql/extension' 23 | require_relative 'sql/role' 24 | 25 | module PostgreSQL 26 | module Cookbook 27 | module SqlHelpers 28 | include Connection 29 | include Attribute 30 | include Database 31 | include Extension 32 | include Role 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /test/cookbooks/test/recipes/extension.rb: -------------------------------------------------------------------------------- 1 | # Dokken images don't have all locales available so this is a workaround 2 | locale = value_for_platform( 3 | %i(debian ubuntu oracle amazon almalinux rocky) => { default: 'C.UTF-8' }, 4 | centos: { default: node['platform_version'].to_i < 8 ? 'en_US.utf-8' : 'C.UTF-8' }, 5 | default: 'en_US' 6 | ) 7 | 8 | postgresql_install 'postgresql' do 9 | version node['test']['pg_ver'] 10 | initdb_locale locale 11 | initdb_encoding 'UTF-8' 12 | 13 | action %i(install init_server) 14 | end 15 | 16 | postgresql_service 'postgresql' do 17 | action %i(enable start) 18 | end 19 | 20 | postgresql_user 'postgres' do 21 | unencrypted_password '12345' 22 | action :nothing 23 | end 24 | 25 | postgresql_database 'test_1' do 26 | locale locale if node['test']['pg_ver'].to_i >= 13 27 | notifies :set_password, 'postgresql_user[postgres]', :immediately 28 | end 29 | 30 | if platform_family?('debian') 31 | package 'postgresql-contrib' 32 | else 33 | package "postgresql#{node['test']['pg_ver'].delete('.')}-contrib" 34 | end 35 | 36 | postgresql_extension 'plpgsql' do 37 | dbname 'test_1' 38 | end 39 | 40 | postgresql_extension 'uuid-ossp' do 41 | dbname 'test_1' 42 | end 43 | -------------------------------------------------------------------------------- /documentation/partial/_connection.md: -------------------------------------------------------------------------------- 1 | # _connection 2 | 3 | [Back to resource list](../README.md#resources) 4 | 5 | ## Actions 6 | 7 | - None 8 | 9 | ## Properties 10 | 11 | | Name | Name? | Type | Default | Description | Allowed Values | 12 | | ------------------- | ----- | ----------- | ------- | ----------------------------------- | -------------- | 13 | | `host` | | String | | PostgreSQL server hostname | | 14 | | `port` | | Integer | | PostgreSQL server port number | | 15 | | `options` | | Hash | | PostgreSQL backend options | | 16 | | `dbname` | | String | | PostgreSQL database name | | 17 | | `user` | | String | | PostgreSQL login user name | | 18 | | `password` | | String | | PostgreSQL login password | | 19 | | `connection_string` | | String | | PostgreSQL server connection string | | 20 | | `force` | | true, false | | SQL command FORCE | | 21 | -------------------------------------------------------------------------------- /resources/partial/_config_file.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Cookbook:: postgresql 3 | # Partial:: _config_file 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | include PostgreSQL::Cookbook::Helpers 19 | 20 | unified_mode true 21 | 22 | property :config_file, String, 23 | desired_state: false 24 | 25 | property :cookbook, String, 26 | default: 'postgresql', 27 | desired_state: false 28 | 29 | property :source, String, 30 | desired_state: false 31 | 32 | property :owner, String, 33 | default: 'postgres' 34 | 35 | property :group, String, 36 | default: 'root' 37 | 38 | property :filemode, String, 39 | default: '0600' 40 | 41 | action_class do 42 | include PostgreSQL::Cookbook::Helpers 43 | end 44 | -------------------------------------------------------------------------------- /test/integration/server_install_os/controls/access.rb: -------------------------------------------------------------------------------- 1 | control 'postgresql-local-access' do 2 | impact 1.0 3 | desc 'This test ensures postgres has localhost access to the database' 4 | 5 | describe postgres_hba_conf.where { type == 'host' && user == 'postgres' } do 6 | its('database') { should cmp 'all' } 7 | its('user') { should cmp 'postgres' } 8 | its('auth_method') { should cmp 'scram-sha-256' } 9 | its('address') { should cmp '127.0.0.1/32' } 10 | end 11 | 12 | postgres_access = postgres_session('postgres', '12345', '127.0.0.1') 13 | 14 | describe postgres_access.query('SELECT 1;') do 15 | its('output') { should eq '1' } 16 | end 17 | end 18 | 19 | control 'postgresql-sous-chef-access' do 20 | impact 1.0 21 | desc 'This test ensures sous_chefs have local trust access to the database' 22 | 23 | describe postgres_hba_conf.where { user == 'sous_chef' } do 24 | its('database') { should cmp 'all' } 25 | its('type') { should cmp 'host' } 26 | its('auth_method') { should cmp 'scram-sha-256' } 27 | its('address') { should cmp '127.0.0.1/32' } 28 | end 29 | 30 | postgres_access = postgres_session('sous_chef', '67890') 31 | 32 | describe postgres_access.query('SELECT 1;', ['postgres']) do 33 | its('output') { should eq '1' } 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /test/cookbooks/test/recipes/server_install.rb: -------------------------------------------------------------------------------- 1 | postgresql_install 'postgresql' do 2 | version node['test']['pg_ver'] 3 | action %i(install init_server) 4 | end 5 | 6 | postgresql_config 'postgresql-server' do 7 | version '15' 8 | 9 | server_config({ 10 | 'max_connections' => 110, 11 | 'shared_buffers' => '128MB', 12 | 'dynamic_shared_memory_type' => 'posix', 13 | 'max_wal_size' => '1GB', 14 | 'min_wal_size' => '80MB', 15 | 'log_destination' => 'stderr', 16 | 'logging_collector' => true, 17 | 'log_directory' => 'log', 18 | 'log_filename' => 'postgresql-%a.log', 19 | 'log_rotation_age' => '1d', 20 | 'log_rotation_size' => 0, 21 | 'log_truncate_on_rotation' => true, 22 | 'log_line_prefix' => '%m [%p]', 23 | 'log_timezone' => 'Etc/UTC', 24 | 'datestyle' => 'iso, mdy', 25 | 'timezone' => 'Etc/UTC', 26 | 'lc_messages' => 'C', 27 | 'lc_monetary' => 'C', 28 | 'lc_numeric' => 'C', 29 | 'lc_time' => 'C', 30 | 'default_text_search_config' => 'pg_catalog.english', 31 | }) 32 | 33 | notifies :restart, 'postgresql_service[postgresql]', :delayed 34 | action :create 35 | end 36 | 37 | postgresql_service 'postgresql' do 38 | action %i(enable start) 39 | end 40 | -------------------------------------------------------------------------------- /templates/default/createcluster.conf.erb: -------------------------------------------------------------------------------- 1 | # 2 | # Generated by Chef for <%= node['fqdn'] %> 3 | # Do NOT modify this file by hand. 4 | # 5 | 6 | # Default values for pg_createcluster(8) 7 | # Occurrences of '%v' are replaced by the major version number, 8 | # and '%c' by the cluster name. Use '%%' for a literal '%'. 9 | 10 | # Create a "main" cluster when a new postgresql-x.y server package is installed 11 | #create_main_cluster = true 12 | 13 | # Default start.conf value, must be one of "auto", "manual", and "disabled". 14 | # See pg_createcluster(8) for more documentation. 15 | #start_conf = 'auto' 16 | 17 | # Default data directory. 18 | #data_directory = '/var/lib/postgresql/%v/%c' 19 | 20 | # Default directory for transaction logs 21 | # Unset by default, i.e. transaction logs remain in the data directory. 22 | #waldir = '/var/lib/postgresql/wal/%v/%c/pg_wal' 23 | 24 | # Options to pass to initdb. 25 | initdb_options = '<%= @initdb_options %>' 26 | 27 | # The following options are copied into the new cluster's postgresql.conf: 28 | 29 | # Enable SSL by default (using the "snakeoil" certificates installed by the 30 | # ssl-cert package, unless configured otherwise here) 31 | ssl = on 32 | 33 | # Show cluster name in process title 34 | cluster_name = '%v/%c' 35 | 36 | # Add prefix to log lines 37 | log_line_prefix = '%%m [%%p] %%q%%u@%%d ' 38 | 39 | # Add "include_dir" in postgresql.conf 40 | add_include_dir = 'conf.d' 41 | 42 | # Directory for additional createcluster config 43 | include_dir '/etc/postgresql-common/createcluster.d' 44 | -------------------------------------------------------------------------------- /libraries/sql/attribute.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Cookbook:: postgresql 3 | # Library:: sql/attribute 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | require_relative '_connection' 19 | require_relative '_utils' 20 | 21 | module PostgreSQL 22 | module Cookbook 23 | module SqlHelpers 24 | module Attribute 25 | private 26 | 27 | include PostgreSQL::Cookbook::SqlHelpers::Connection 28 | 29 | def pg_attribute?(role, attribute, value) 30 | sql = 'SELECT rolconfig FROM pg_roles WHERE rolname=$1' 31 | attribute_config = execute_sql_params(sql, [ role ], max_one_result: true).fetch('rolconfig') 32 | Chef::Log.warn("AC: #{attribute_config}") 33 | 34 | attribute_config = config_string_to_hash(attribute_config) 35 | map_pg_values!(attribute_config) 36 | 37 | Chef::Log.warn("AC Parsed: #{attribute_config}") 38 | Chef::Log.warn("AC Testing: #{attribute} | #{value}") 39 | 40 | attribute_config.fetch(attribute).eql?(value) 41 | end 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /test/cookbooks/test/recipes/all_repos_install.rb: -------------------------------------------------------------------------------- 1 | postgresql_install 'postgresql' do 2 | version node['test']['pg_ver'] 3 | setup_repo_pgdg_source true 4 | setup_repo_pgdg_updates_testing true 5 | setup_repo_pgdg_source_updates_testing true 6 | action %i(install init_server) 7 | end 8 | 9 | postgresql_config 'postgresql-server' do 10 | version '15' 11 | 12 | server_config({ 13 | 'max_connections' => 110, 14 | 'shared_buffers' => '128MB', 15 | 'dynamic_shared_memory_type' => 'posix', 16 | 'max_wal_size' => '1GB', 17 | 'min_wal_size' => '80MB', 18 | 'log_destination' => 'stderr', 19 | 'logging_collector' => true, 20 | 'log_directory' => 'log', 21 | 'log_filename' => 'postgresql-%a.log', 22 | 'log_rotation_age' => '1d', 23 | 'log_rotation_size' => 0, 24 | 'log_truncate_on_rotation' => true, 25 | 'log_line_prefix' => '%m [%p]', 26 | 'log_timezone' => 'Etc/UTC', 27 | 'datestyle' => 'iso, mdy', 28 | 'timezone' => 'Etc/UTC', 29 | 'lc_messages' => 'C', 30 | 'lc_monetary' => 'C', 31 | 'lc_numeric' => 'C', 32 | 'lc_time' => 'C', 33 | 'default_text_search_config' => 'pg_catalog.english', 34 | }) 35 | 36 | notifies :restart, 'postgresql_service[postgresql]', :delayed 37 | action :create 38 | end 39 | 40 | postgresql_service 'postgresql' do 41 | action %i(enable start) 42 | end 43 | -------------------------------------------------------------------------------- /Dangerfile: -------------------------------------------------------------------------------- 1 | # Reference: http://danger.systems/reference.html 2 | 3 | # A pull request summary is required. Add a description of the pull request purpose. 4 | # Changelog must be updated for each pull request that changes code. 5 | # Warnings will be issued for: 6 | # Pull request with more than 400 lines of code changed 7 | # Pull reqest that change more than 5 lines without test changes 8 | # Failures will be issued for: 9 | # Pull request without summary 10 | # Pull requests with code changes without changelog entry 11 | 12 | def code_changes? 13 | code = %w(libraries attributes recipes resources files templates) 14 | code.each do |location| 15 | return true unless git.modified_files.grep(/#{location}/).empty? 16 | end 17 | false 18 | end 19 | 20 | def test_changes? 21 | tests = %w(spec test kitchen.yml kitchen.dokken.yml) 22 | tests.each do |location| 23 | return true unless git.modified_files.grep(/#{location}/).empty? 24 | end 25 | false 26 | end 27 | 28 | failure 'Please provide a summary of your Pull Request.' if github.pr_body.length < 10 29 | 30 | warn 'This is a big Pull Request.' if git.lines_of_code > 400 31 | 32 | warn 'This is a Table Flip.' if git.lines_of_code > 2000 33 | 34 | # Require a CHANGELOG entry for non-test changes. 35 | if !git.modified_files.include?('CHANGELOG.md') && code_changes? 36 | failure 'Please include a CHANGELOG entry.' 37 | end 38 | 39 | # Require Major Minor Patch version labels 40 | unless github.pr_labels.grep /minor|major|patch/i 41 | warn 'Please add a release label to this pull request' 42 | end 43 | 44 | # A sanity check for tests. 45 | if git.lines_of_code > 5 && code_changes? && !test_changes? 46 | warn 'This Pull Request is probably missing tests.' 47 | end 48 | -------------------------------------------------------------------------------- /resources/service.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Cookbook:: postgresql 3 | # Resource:: service 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | unified_mode true 19 | 20 | include PostgreSQL::Cookbook::Helpers 21 | 22 | property :service_name, String, 23 | default: lazy { default_platform_service_name }, 24 | description: 'Service name to perform actions for' 25 | 26 | property :delay_start, [true, false], 27 | default: true, 28 | description: 'Delay service start until end of run' 29 | 30 | action_class do 31 | def do_service_action(resource_action) 32 | if %i(start restart reload).include?(resource_action) && new_resource.delay_start 33 | declare_resource(:service, new_resource.service_name) do 34 | supports status: true, restart: true, reload: true 35 | 36 | delayed_action resource_action 37 | end 38 | else 39 | declare_resource(:service, new_resource.service_name) do 40 | supports status: true, restart: true, reload: true 41 | 42 | action resource_action 43 | end 44 | end 45 | end 46 | end 47 | 48 | %i(start stop restart reload enable disable).each do |action_type| 49 | send(:action, action_type) { do_service_action(action) } 50 | end 51 | -------------------------------------------------------------------------------- /resources/partial/_connection.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Cookbook:: postgresql 3 | # Partial:: _connection 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | unified_mode true 19 | 20 | property :host, String, 21 | desired_state: false, 22 | description: 'PostgreSQL server hostname' 23 | 24 | property :port, Integer, 25 | default: 5432, 26 | desired_state: false, 27 | description: 'PostgreSQL server port number' 28 | 29 | property :options, Hash, 30 | desired_state: false, 31 | description: 'PostgreSQL backend options' 32 | 33 | property :dbname, String, 34 | desired_state: false, 35 | description: 'PostgreSQL database name' 36 | 37 | property :user, String, 38 | default: 'postgres', 39 | desired_state: false, 40 | description: 'PostgreSQL login user name' 41 | 42 | property :password, String, 43 | desired_state: false, 44 | description: 'PostgreSQL login password' 45 | 46 | property :connection_string, String, 47 | desired_state: false, 48 | description: 'PostgreSQL server connection string' 49 | 50 | property :force, [true, false], 51 | desired_state: false, 52 | description: 'SQL command FORCE' 53 | -------------------------------------------------------------------------------- /chefignore: -------------------------------------------------------------------------------- 1 | # Put files/directories that should be ignored in this file when uploading 2 | # to a Chef Infra Server or Supermarket. 3 | # Lines that start with '# ' are comments. 4 | 5 | # OS generated files # 6 | ###################### 7 | .DS_Store 8 | ehthumbs.db 9 | Icon? 10 | nohup.out 11 | Thumbs.db 12 | .envrc 13 | 14 | # EDITORS # 15 | ########### 16 | .#* 17 | .project 18 | .settings 19 | *_flymake 20 | *_flymake.* 21 | *.bak 22 | *.sw[a-z] 23 | *.tmproj 24 | *~ 25 | \#* 26 | REVISION 27 | TAGS* 28 | tmtags 29 | .vscode 30 | .editorconfig 31 | 32 | ## COMPILED ## 33 | ############## 34 | *.class 35 | *.com 36 | *.dll 37 | *.exe 38 | *.o 39 | *.pyc 40 | *.so 41 | */rdoc/ 42 | a.out 43 | mkmf.log 44 | 45 | # Testing # 46 | ########### 47 | .circleci/* 48 | .codeclimate.yml 49 | .delivery/* 50 | .foodcritic 51 | .kitchen* 52 | .mdlrc 53 | .overcommit.yml 54 | .rspec 55 | .rubocop.yml 56 | .travis.yml 57 | .watchr 58 | .yamllint 59 | azure-pipelines.yml 60 | Dangerfile 61 | examples/* 62 | features/* 63 | Guardfile 64 | kitchen*.yml 65 | mlc_config.json 66 | Procfile 67 | Rakefile 68 | spec/* 69 | test/* 70 | 71 | # SCM # 72 | ####### 73 | .git 74 | .gitattributes 75 | .gitconfig 76 | .github/* 77 | .gitignore 78 | .gitkeep 79 | .gitmodules 80 | .svn 81 | */.bzr/* 82 | */.git 83 | */.hg/* 84 | */.svn/* 85 | 86 | # Berkshelf # 87 | ############# 88 | Berksfile 89 | Berksfile.lock 90 | cookbooks/* 91 | tmp 92 | 93 | # Bundler # 94 | ########### 95 | vendor/* 96 | Gemfile 97 | Gemfile.lock 98 | 99 | # Policyfile # 100 | ############## 101 | Policyfile.rb 102 | Policyfile.lock.json 103 | 104 | # Documentation # 105 | ############# 106 | CODE_OF_CONDUCT* 107 | CONTRIBUTING* 108 | documentation/* 109 | TESTING* 110 | UPGRADING* 111 | 112 | # Vagrant # 113 | ########### 114 | .vagrant 115 | Vagrantfile 116 | -------------------------------------------------------------------------------- /libraries/sql/_utils.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Cookbook:: postgresql 3 | # Library:: sql/_utils 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | require_relative '../_utils' 19 | 20 | module PostgreSQL 21 | module Cookbook 22 | module SqlHelpers 23 | module Utils 24 | private 25 | 26 | include PostgreSQL::Cookbook::Utils 27 | 28 | def config_string_to_hash(string) 29 | return {} if nil_or_empty?(string) 30 | 31 | config_string = string.dup 32 | config_string.delete_prefix!('{') 33 | config_string.delete_suffix!('}') 34 | config_string = config_string.split(',').map { |rcv| rcv.split('=') }.to_h 35 | 36 | config_string.transform_values! do |v| 37 | case v 38 | when 'on' 39 | true 40 | when 'off' 41 | false 42 | else v 43 | end 44 | end 45 | 46 | config_string 47 | end 48 | 49 | def map_pg_values!(hash) 50 | raise ArgumentError unless hash.is_a?(Hash) 51 | 52 | hash.transform_values! do |v| 53 | case v 54 | when 't' 55 | true 56 | when 'f' 57 | false 58 | else 59 | v 60 | end 61 | end 62 | end 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /libraries/_utils.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Cookbook:: postgresql 3 | # Library:: _utils 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | module PostgreSQL 19 | module Cookbook 20 | module Utils 21 | AUTH_OPTION_REGEX = /[\w-]+=(?:"[^"]*"|[^\s"]+)/ 22 | 23 | private 24 | 25 | # Converts the given HBA auth-options Hash or String into its sorted string version 26 | # 27 | # @return [string] the alphanumerically sorted list of auth-options 28 | def sorted_auth_options_string(value) 29 | case value 30 | when Hash 31 | value.map { |k, v| "#{k}=#{v}" }.sort!.join(' ') 32 | when String 33 | value.scan(AUTH_OPTION_REGEX).sort!.join(' ') 34 | else 35 | raise ArgumentError, "Only Hash and String are supported, #{value.class} given." 36 | end 37 | end 38 | 39 | # Check if a given object(s) are either Nil or Empty 40 | # 41 | # @return [true, false] Nil or Empty check result 42 | # 43 | def nil_or_empty?(*values) 44 | values.any? { |v| v.nil? || (v.respond_to?(:empty?) && v.empty?) } 45 | end 46 | 47 | # Check if a given gem is installed and available for require 48 | # 49 | # @return [true, false] Gem installed result 50 | # 51 | def gem_installed?(gem_name) 52 | !Gem::Specification.find_by_name(gem_name).nil? 53 | rescue Gem::LoadError 54 | false 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /documentation/postgresql_config.md: -------------------------------------------------------------------------------- 1 | # postgresql_config 2 | 3 | [Back to resource list](../README.md#resources) 4 | 5 | This resource manages the postgresql.conf configuration file. 6 | 7 | ## Requires 8 | 9 | - `deepsort` 10 | 11 | ## Uses 12 | 13 | - [partial/_config_file](partial/_config_file.md) 14 | 15 | ## Actions 16 | 17 | - `:create` 18 | - `:delete` 19 | 20 | ## Properties 21 | 22 | | Name | Name? | Type | Default | Description | Allowed Values | 23 | | ------------------- | ----- | --------------- | ------- | --------------------------------------- | -------------- | 24 | | `config_file` | | String | | | | 25 | | `source` | | String | | | | 26 | | `version` | | String, Integer | | PostgreSQL installed version override | | 27 | | `data_directory` | | String | | PostgreSQL server data directory | | 28 | | `hba_file` | | String | | PostgreSQL pg_hba.conf file location | | 29 | | `ident_file` | | String | | PostgreSQL pg_ident.conf file location | | 30 | | `external_pid_file` | | String | | PostgreSQL external PID file location | | 31 | | `server_config` | | Hash | | PostgreSQL server configuration options | | 32 | 33 | ## Examples 34 | 35 | Setup the PostgreSQL configuration with a specific data directory: 36 | 37 | > Note: If you have installed a specific version of PostgreSQL (different from the default version), you must specify that version in this resource too 38 | 39 | ```ruby 40 | postgresql_server_conf 'My PostgreSQL Config' do 41 | version '15' 42 | data_directory '/data/postgresql/15/main' 43 | notifies :reload, 'postgresql_service[postgresql]' 44 | end 45 | ``` 46 | -------------------------------------------------------------------------------- /libraries/config.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Cookbook:: postgresql 3 | # Library:: config 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | require 'deepsort' 19 | require 'inifile' 20 | 21 | module PostgreSQL 22 | module Cookbook 23 | module ConfigHelpers 24 | include PostgreSQL::Cookbook::Utils 25 | 26 | extend self 27 | 28 | # Enumerable deep clean proc 29 | ENUM_DEEP_CLEAN = proc do |*args| 30 | v = args.last 31 | v.delete_if(&ENUM_DEEP_CLEAN) if v.respond_to?(:delete_if) 32 | nil_or_empty?(v) && !v.is_a?(String) 33 | end 34 | 35 | # Load an INI file from disk 36 | # 37 | # @param file [String] The file to load 38 | # @return [Hash] File contents 39 | # 40 | def postgresql_conf_load_file(file) 41 | return unless ::File.exist?(file) 42 | 43 | ::IniFile.load(file).to_h 44 | end 45 | 46 | # Create an INI file output as a String from a Hash 47 | # 48 | # @param content [Hash] The file contents as a Hash 49 | # @return [String] Formatted INI output 50 | # 51 | def postgresql_conf_file_string(content, sort = true) 52 | raise ArgumentError, "Expected Hash got #{content.class}" unless content.is_a?(Hash) 53 | 54 | content_compact = content.dup.compact 55 | content_compact.deep_sort! if sort 56 | content_compact.delete_if(&ENUM_DEEP_CLEAN) 57 | 58 | ::IniFile.new(content: { 'global' => content_compact }).to_s.gsub("[global]\n", '') 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /kitchen.dokken.yml: -------------------------------------------------------------------------------- 1 | driver: 2 | name: dokken 3 | privileged: true 4 | chef_version: <%= ENV['CHEF_VERSION'] || 'current' %> 5 | 6 | transport: { name: dokken } 7 | provisioner: { name: dokken } 8 | 9 | platforms: 10 | - name: almalinux-9 11 | driver: 12 | image: dokken/almalinux-9 13 | pid_one_command: /usr/lib/systemd/systemd 14 | 15 | - name: almalinux-10 16 | driver: 17 | image: dokken/almalinux-10 18 | pid_one_command: /usr/lib/systemd/systemd 19 | 20 | - name: amazonlinux-2023 21 | driver: 22 | image: dokken/amazonlinux-2023 23 | pid_one_command: /usr/lib/systemd/systemd 24 | 25 | - name: centos-stream-9 26 | driver: 27 | image: dokken/centos-stream-9 28 | pid_one_command: /usr/lib/systemd/systemd 29 | 30 | - name: centos-stream-10 31 | driver: 32 | image: dokken/centos-stream-10 33 | pid_one_command: /usr/lib/systemd/systemd 34 | 35 | - name: debian-12 36 | driver: 37 | image: dokken/debian-12 38 | pid_one_command: /bin/systemd 39 | 40 | - name: debian-13 41 | driver: 42 | image: dokken/debian-13 43 | pid_one_command: /usr/lib/systemd/systemd 44 | 45 | - name: fedora-latest 46 | driver: 47 | image: dokken/fedora-latest 48 | pid_one_command: /usr/lib/systemd/systemd 49 | 50 | - name: opensuse-leap-15 51 | driver: 52 | image: dokken/opensuse-leap-15 53 | pid_one_command: /usr/lib/systemd/systemd 54 | 55 | - name: oraclelinux-9 56 | driver: 57 | image: dokken/oraclelinux-9 58 | pid_one_command: /usr/lib/systemd/systemd 59 | 60 | - name: rockylinux-9 61 | driver: 62 | image: dokken/rockylinux-9 63 | pid_one_command: /usr/lib/systemd/systemd 64 | 65 | - name: rockylinux-10 66 | driver: 67 | image: dokken/rockylinux-10 68 | pid_one_command: /usr/lib/systemd/systemd 69 | 70 | - name: ubuntu-22.04 71 | driver: 72 | image: dokken/ubuntu-22.04 73 | pid_one_command: /bin/systemd 74 | 75 | - name: ubuntu-24.04 76 | driver: 77 | image: dokken/ubuntu-24.04 78 | pid_one_command: /bin/systemd 79 | -------------------------------------------------------------------------------- /test/cookbooks/test/recipes/no_repos_install.rb: -------------------------------------------------------------------------------- 1 | # I can do it myself! 2 | yum_repository 'postgresql-15' do 3 | baseurl 'https://download.postgresql.org/pub/repos/yum/15/redhat/rhel-$releasever-$basearch' 4 | gpgkey "https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-RHEL#{node['platform_version'].to_i.eql?(7) ? '7' : ''}" 5 | end 6 | 7 | yum_repository 'postgresql-common' do 8 | baseurl 'https://download.postgresql.org/pub/repos/yum/common/redhat/rhel-$releasever-$basearch' 9 | gpgkey "https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-RHEL#{node['platform_version'].to_i.eql?(7) ? '7' : ''}" 10 | end 11 | 12 | postgresql_install 'postgresql' do 13 | version node['test']['pg_ver'] 14 | repo_pgdg false 15 | repo_pgdg_common false 16 | action %i(install init_server) 17 | end 18 | 19 | postgresql_config 'postgresql-server' do 20 | version '15' 21 | 22 | server_config({ 23 | 'max_connections' => 110, 24 | 'shared_buffers' => '128MB', 25 | 'dynamic_shared_memory_type' => 'posix', 26 | 'max_wal_size' => '1GB', 27 | 'min_wal_size' => '80MB', 28 | 'log_destination' => 'stderr', 29 | 'logging_collector' => true, 30 | 'log_directory' => 'log', 31 | 'log_filename' => 'postgresql-%a.log', 32 | 'log_rotation_age' => '1d', 33 | 'log_rotation_size' => 0, 34 | 'log_truncate_on_rotation' => true, 35 | 'log_line_prefix' => '%m [%p]', 36 | 'log_timezone' => 'Etc/UTC', 37 | 'datestyle' => 'iso, mdy', 38 | 'timezone' => 'Etc/UTC', 39 | 'lc_messages' => 'C', 40 | 'lc_monetary' => 'C', 41 | 'lc_numeric' => 'C', 42 | 'lc_time' => 'C', 43 | 'default_text_search_config' => 'pg_catalog.english', 44 | }) 45 | 46 | notifies :restart, 'postgresql_service[postgresql]', :delayed 47 | action :create 48 | end 49 | 50 | postgresql_service 'postgresql' do 51 | action %i(enable start) 52 | end 53 | -------------------------------------------------------------------------------- /UPGRADING.md: -------------------------------------------------------------------------------- 1 | # Upgrading 2 | 3 | ## Upgrading to v11.0 4 | 5 | Version 11 is a major refactor and will require wrapping cookbooks to be updated to match the resource changes. 6 | 7 | ### Summary 8 | 9 | - All resources now provide `load_current_value` support so will report changes on converge and will fire notifications. 10 | - To provide this, all database access has been switched back to using the `pg` gem 11 | - The `:client_install`, `:server_install` and `:repository` resources have been condensed into a single `:install` resource providing the same install functionality 12 | - Service actions have been removed from the install resource 13 | - The `:server_conf` resource have been renamed to `:config` and server configuration is expected to be passed as a Hash to the `:server_config` property 14 | - This change has been made to support later PostgreSQL version where some configuration values may have been removed 15 | - A `:service` resource is provided to manage PostgreSQL service management 16 | - Various resource properties have been renamed to remove collisions and match the PostgreSQL documentation and system column names 17 | - The `:access` and `:ident` resources now use a persistent accumulator so removals require an explicit `:delete` action 18 | 19 | ## Upgrading from v6.0 20 | 21 | From v7.0.0 of the postgresql cookbook we have removed all recipes and attributes from the cookbook. 22 | 23 | ### Deprecations 24 | 25 | #### Gem 26 | 27 | Due to limitations in the gem compile process (libssl related) we have removed the pg_gem. 28 | 29 | We no longer support accessing the database via the gem, and internally use the cli. 30 | 31 | #### PG Tune 32 | 33 | We currently do not implement the PG Tune functionality. 34 | 35 | This may be added in a future release. 36 | 37 | ### Major Changes 38 | 39 | Recipes are no longer supported so you should take a look at the examples in `test/cookbooks/test/recipes` 40 | 41 | An example of how to 42 | 43 | - Install the the server from the postgresql repository 44 | - Install a database 45 | - Install an extension 46 | 47 | install an extension 48 | 49 | ```ruby 50 | postgresql_repository 'install' 51 | 52 | postgresql_server_install 'package' 53 | 54 | postgresql_database 'test_1' 55 | 56 | postgresql_extension 'openfts' do 57 | database 'test_1' 58 | end 59 | ``` 60 | -------------------------------------------------------------------------------- /test/cookbooks/test/recipes/ident.rb: -------------------------------------------------------------------------------- 1 | postgresql_install 'postgresql' do 2 | version node['test']['pg_ver'] 3 | 4 | action %i(install init_server) 5 | end 6 | 7 | postgresql_access 'local all all peer delete' do 8 | type 'local' 9 | database 'all' 10 | user 'all' 11 | auth_method 'peer' 12 | 13 | action :delete 14 | end 15 | 16 | postgresql_service 'postgresql' do 17 | action %i(enable start) 18 | end 19 | 20 | user 'shef' do 21 | shell '/bin/bash' 22 | manage_home true 23 | end 24 | 25 | postgresql_ident 'postgresl mapping' do 26 | map_name 'testmap1' 27 | system_username 'postgres' 28 | database_username 'postgres' 29 | comment 'Postgresql 1 test mapping' 30 | 31 | notifies :reload, 'postgresql_service[postgresql]', :delayed 32 | end 33 | 34 | postgresql_ident 'shef mapping' do 35 | map_name 'testmap2' 36 | system_username 'shef' 37 | database_username 'sous_chef' 38 | 39 | notifies :reload, 'postgresql_service[postgresql]', :delayed 40 | end 41 | 42 | postgresql_ident 'shef remove mapping' do 43 | map_name 'testmap3' 44 | system_username 'shef_remove' 45 | database_username 'sous_chef' 46 | 47 | notifies :reload, 'postgresql_service[postgresql]', :delayed 48 | action :delete 49 | end 50 | 51 | postgresql_access 'postgresql host superuser' do 52 | type 'host' 53 | database 'all' 54 | user 'postgres' 55 | address '127.0.0.1/32' 56 | auth_method 'scram-sha-256' 57 | 58 | notifies :reload, 'postgresql_service[postgresql]', :delayed 59 | end 60 | 61 | postgresql_access 'shef mapping' do 62 | type 'local' 63 | database 'all' 64 | user 'sous_chef' 65 | auth_method 'peer' 66 | auth_options 'map=testmap2' 67 | cookbook 'test' 68 | 69 | notifies :reload, 'postgresql_service[postgresql]', :delayed 70 | end 71 | 72 | postgresql_user 'sous_chef' do 73 | superuser true 74 | login true 75 | password '67890' 76 | sensitive false 77 | 78 | notifies :set_password, 'postgresql_user[postgres]', :delayed 79 | end 80 | 81 | postgresql_user 'sous_chef' do 82 | superuser false 83 | connection_limit 5 84 | action :update 85 | end 86 | 87 | postgresql_user 'postgres' do 88 | unencrypted_password '12345' 89 | action :nothing 90 | end 91 | 92 | postgresql_database 'test1' 93 | 94 | postgresql_database 'test2' do 95 | action :delete 96 | end 97 | 98 | postgresql_extension 'plpgsql' 99 | -------------------------------------------------------------------------------- /resources/extension.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Cookbook:: postgresql 3 | # Resource:: extension 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | unified_mode true 19 | 20 | use 'partial/_connection' 21 | 22 | property :extension, String, 23 | name_property: true, 24 | description: 'The name of the extension to be installed' 25 | 26 | property :schema, String, 27 | description: 'The name of the schema in which to install the extension objects' 28 | 29 | property :old_version, String, 30 | description: 'old_version must be specified when, and only when, you are attempting to install an extension that replaces an "old style" module that is just a collection of objects not packaged into an extension.' 31 | 32 | property :version, String, 33 | description: 'The version of the extension to install' 34 | 35 | property :cascade, [true, false], 36 | description: 'Automatically install any extensions that this extension depends on that are not already installed' 37 | 38 | property :restrict, [true, false], 39 | description: 'This option prevents the specified extensions from being dropped if other objects, besides these extensions, their members, and their explicitly dependent routines, depend on them' 40 | 41 | include PostgreSQL::Cookbook::SqlHelpers::Extension 42 | 43 | load_current_value do |new_resource| 44 | current_value_does_not_exist! unless pg_extension?(new_resource) 45 | 46 | extension_data = pg_extension(new_resource.extension) 47 | 48 | version(extension_data.fetch('extversion', nil)) 49 | end 50 | 51 | action_class do 52 | include PostgreSQL::Cookbook::SqlHelpers::Extension 53 | end 54 | 55 | action :create do 56 | converge_if_changed { create_extension(new_resource) } 57 | end 58 | 59 | action :drop do 60 | converge_by("Drop extension #{new_resource.extension}") { drop_extension(new_resource) } if pg_extension?(new_resource) 61 | end 62 | 63 | action :delete do 64 | run_action(:drop) 65 | end 66 | -------------------------------------------------------------------------------- /.github/instructions/project.instructions.md: -------------------------------------------------------------------------------- 1 | # Copilot Project Instructions: postgresql Cookbook (Specific) 2 | 3 | This file provides cookbook-specific instructions for Copilot and other AI coding assistants. It supplements, but does not duplicate, the general instructions in `.github/copilot-instructions.md`. 4 | 5 | ## Cookbook Purpose 6 | 7 | - The `postgresql` cookbook manages installation, configuration, and access control for PostgreSQL database servers. 8 | - It provides custom Chef resources for PostgreSQL installation, service management, user/database creation, access control, and configuration. 9 | - Supports multiple PostgreSQL versions and major Linux distributions (see `metadata.rb`, `kitchen.yml`). 10 | 11 | ## Key Custom Resources 12 | 13 | - `postgresql_install`: Installs and initializes PostgreSQL server. 14 | - `postgresql_service`: Manages the PostgreSQL system service (start, stop, restart, enable, etc.). 15 | - `postgresql_access`: Manages entries in `pg_hba.conf` for access control. 16 | - `postgresql_user`: Creates, updates, and manages PostgreSQL users and roles. 17 | - `postgresql_database`: Creates and manages databases. 18 | - `postgresql_config`: Manages configuration files. 19 | - `postgresql_extension`, `postgresql_ident`, `postgresql_role`: Additional resources for advanced PostgreSQL features. 20 | 21 | ## Cookbook-Specific Patterns 22 | 23 | - All access control changes (`postgresql_access`) must ensure the config file is written before triggering a service restart. 24 | - Helper modules in `libraries/` are used for parsing and manipulating PostgreSQL config files. 25 | - Example usage and test coverage for all resources is found in `test/cookbooks/test/recipes/`. 26 | - Templates for configuration files are located in `templates/default/`. 27 | 28 | ## Testing and Validation 29 | 30 | - Integration tests cover multiple OS and PostgreSQL versions (see `.kitchen/logs/` for matrix). 31 | - Example test recipes demonstrate resource usage and edge cases (e.g., usernames with dashes, multiple databases/users, LDAP auth). 32 | 33 | ## Documentation 34 | 35 | - Resource documentation is in `documentation/` and includes usage examples and property details for each resource. 36 | 37 | ## Special Notes 38 | 39 | - This cookbook is resource-driven; recipes are only used for testing and examples. 40 | - All changes must maintain idempotency and proper notification sequencing for service restarts. 41 | - Do not bypass resource logic—always use the provided custom resources for PostgreSQL management. 42 | 43 | For general build, test, and workflow instructions, refer to `.github/copilot-instructions.md`. 44 | -------------------------------------------------------------------------------- /libraries/sql/extension.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Cookbook:: postgresql 3 | # Library:: sql/extension 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | require_relative '../_utils' 19 | require_relative '_connection' 20 | 21 | module PostgreSQL 22 | module Cookbook 23 | module SqlHelpers 24 | module Extension 25 | private 26 | 27 | include PostgreSQL::Cookbook::Utils 28 | include PostgreSQL::Cookbook::SqlHelpers::Connection 29 | 30 | def pg_extension(name) 31 | sql = 'SELECT * FROM pg_extension WHERE extname=$1' 32 | 33 | execute_sql_params(sql, [ name ], max_one_result: true).pop 34 | end 35 | 36 | def pg_extension?(new_resource) 37 | sql = 'SELECT extversion FROM pg_extension WHERE extname=$1' 38 | version = execute_sql_params(sql, [ new_resource.extension ], max_one_result: true) 39 | 40 | return false if nil_or_empty?(version) 41 | 42 | version = version.pop 43 | 44 | if new_resource.version 45 | return version.fetch('extversion').eql?(new_resource.version) 46 | end 47 | 48 | !version.empty? 49 | end 50 | 51 | def create_extension(new_resource) 52 | sql = [] 53 | 54 | sql.push("CREATE EXTENSION \"#{new_resource.extension}\"") 55 | 56 | sql.push("SCHEMA \"#{new_resource.schema}\"") if property_is_set?(:schema) 57 | sql.push("VERSION \"#{new_resource.version}\"") if property_is_set?(:version) 58 | sql.push("FROM \"#{new_resource.old_version}\"") if property_is_set?(:old_version) 59 | sql.push('CASCADE') if new_resource.cascade 60 | 61 | execute_sql("#{sql.join(' ').strip};") 62 | end 63 | 64 | def drop_extension(new_resource) 65 | sql = [] 66 | 67 | sql.push("DROP EXTENSION #{new_resource.extension}") 68 | sql.push('CASCADE') if new_resource.cascade 69 | sql.push('RESTRICT') if new_resource.restrict 70 | 71 | execute_sql("#{sql.join(' ').strip};") 72 | end 73 | end 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /test/cookbooks/test/recipes/server_install_os.rb: -------------------------------------------------------------------------------- 1 | postgresql_install 'postgresql' do 2 | source :os 3 | version '15' 4 | action %i(install init_server) 5 | end 6 | 7 | apt_update 8 | 9 | package 'libpq-devel' if platform_family?('amazon') 10 | 11 | postgresql_config 'postgresql-server' do 12 | server_config({ 13 | max_connections: 110, 14 | shared_buffers: '128MB', 15 | log_destination: 'stderr', 16 | logging_collector: true, 17 | log_directory: 'log', 18 | log_filename: 'postgresql-%a.log', 19 | log_rotation_age: '1d', 20 | log_rotation_size: 0, 21 | log_truncate_on_rotation: true, 22 | log_line_prefix: '%m [%p]', 23 | log_timezone: 'Etc/UTC', 24 | datestyle: 'iso, mdy', 25 | timezone: 'Etc/UTC', 26 | lc_messages: 'C', 27 | lc_monetary: 'C', 28 | lc_numeric: 'C', 29 | lc_time: 'C', 30 | default_text_search_config: 'pg_catalog.english', 31 | }) 32 | 33 | notifies :restart, 'postgresql_service[postgresql]', :delayed 34 | action :create 35 | end 36 | 37 | postgresql_service 'postgresql' do 38 | action %i(enable start) 39 | end 40 | 41 | postgresql_access 'postgresql host superuser' do 42 | type 'host' 43 | database 'all' 44 | user 'postgres' 45 | address '127.0.0.1/32' 46 | auth_method 'scram-sha-256' 47 | end 48 | 49 | postgresql_user 'postgres' do 50 | unencrypted_password '12345' 51 | action :nothing 52 | end 53 | 54 | postgresql_user 'sous_chef' do 55 | unencrypted_password '12345' 56 | 57 | notifies :set_password, 'postgresql_user[postgres]', :immediately 58 | end 59 | 60 | postgresql_user 'sous_chef' do 61 | superuser true 62 | unencrypted_password '67890' 63 | config({ statement_timeout: '8min' }) 64 | login true 65 | sensitive false 66 | action :update 67 | end 68 | 69 | postgresql_database 'sous_chef' do 70 | template 'template0' 71 | encoding 'utf8' 72 | end 73 | 74 | postgresql_database 'test1' do 75 | action :create 76 | end 77 | 78 | postgresql_access 'a sous_chef local superuser' do 79 | type 'host' 80 | database 'all' 81 | user 'sous_chef' 82 | auth_method 'scram-sha-256' 83 | address '127.0.0.1/32' 84 | 85 | notifies :restart, 'postgresql_service[postgresql]', :delayed 86 | end 87 | 88 | postgresql_ident 'postgresl mapping' do 89 | map_name 'testmap1' 90 | system_username 'postgres' 91 | database_username 'postgres' 92 | comment 'Postgresql 1 test mapping' 93 | 94 | notifies :reload, 'postgresql_service[postgresql]', :delayed 95 | end 96 | -------------------------------------------------------------------------------- /documentation/postgresql_ident.md: -------------------------------------------------------------------------------- 1 | # postgresql_ident 2 | 3 | [Back to resource list](../README.md#resources) 4 | 5 | This resource generate `pg_ident.conf` configuration file to manage user mapping between system and PostgreSQL users. 6 | 7 | ## Uses 8 | 9 | - [partial/_config_file](partial/_config_file.md) 10 | 11 | ## Actions 12 | 13 | - `:create` - Creates a mapping line inside of `pg_ident.conf` 14 | - `:update` - Update a mapping line inside of `pg_ident.conf` 15 | - `:delete` - Delete a mapping line inside of `pg_ident.conf` 16 | 17 | ## Properties 18 | 19 | | Name | Name? | Type | Default | Description | Allowed Values | 20 | | ------------------- | ----- | ------ | ------- | ------------------------------------------------------------------------ | -------------- | 21 | | `config_file` | | String | | | | 22 | | `source` | | String | | | | 23 | | `map_name` | ✓ | String | | Arbitrary name that will be used to refer to this mapping in pg_hba.conf | | 24 | | `system_username` | | String | | Operating system user name | | 25 | | `database_username` | | String | | Database user name | | 26 | | `comment` | | String | | Ident mapping record comment | | 27 | 28 | ## Libraries 29 | 30 | - `PostgreSQL::Cookbook::IdentHelpers::PgIdentTemplate` 31 | 32 | ## Examples 33 | 34 | Creates a `mymapping` mapping that map `john` system user to `user1` PostgreSQL user: 35 | 36 | ```ruby 37 | postgresql_ident 'Map john to user1' do 38 | comment 'John Mapping' 39 | map_name 'mymapping' 40 | system_username 'john' 41 | database_username 'user1' 42 | end 43 | ``` 44 | 45 | This generates the following line in the `pg_ident.conf`: 46 | 47 | ```config 48 | # MAPNAME SYSTEM-USERNAME PG-USERNAME 49 | 50 | # John Mapping 51 | mymapping john user1 # John Mapping 52 | ``` 53 | 54 | To grant access to the foo user with password authentication: 55 | 56 | ```ruby 57 | postgresql_access 'local_foo_user' do 58 | comment 'Foo user access' 59 | type 'host' 60 | database 'all' 61 | user 'foo' 62 | address '127.0.0.1/32' 63 | auth_method 'scram-sha-256' 64 | end 65 | ``` 66 | 67 | This generates the following line in the `pg_hba.conf`: 68 | 69 | ```config 70 | # Local postgres superuser access 71 | host all foo 127.0.0.1/32 ident # Foo user access 72 | ``` 73 | -------------------------------------------------------------------------------- /documentation/postgresql_extension.md: -------------------------------------------------------------------------------- 1 | # postgresql_extension 2 | 3 | [Back to resource list](../README.md#resources) 4 | 5 | This resource manages PostgreSQL extensions for a given database 6 | 7 | ## Uses 8 | 9 | - [partial/_connection](partial/_connection.md) 10 | 11 | ## Actions 12 | 13 | - `:create` - Creates an extension in a given database 14 | - `:drop` - Drops an extension from the database 15 | - `:delete` - Alias for `:drop` 16 | 17 | ## Properties 18 | 19 | | Name | Name? | Type | Default | Description | Allowed Values | 20 | | ------------- | ----- | ----------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------- | 21 | | `extension` | ✓ | String | | The name of the extension to be installed | | 22 | | `schema` | | String | | The name of the schema in which to install the extension objects | | 23 | | `old_version` | | String | | old_version must be specified when, and only when, you are attempting to install an extension that replaces an "old style" module that is just a collection of objects not packaged into an extension. | | 24 | | `version` | | String | | The version of the extension to install | | 25 | | `cascade` | | true, false | | Automatically install any extensions that this extension depends on that are not already installed | | 26 | | `restrict` | | true, false | | This option prevents the specified extensions from being dropped if other objects, besides these extensions, their members, and their explicitly dependent routines, depend on them | | 27 | 28 | ## Libraries 29 | 30 | - `PostgreSQL::Cookbook::SqlHelpers::Extension` 31 | 32 | ## Examples 33 | 34 | To install the `adminpack` extension: 35 | 36 | ```ruby 37 | # Add the contrib package in Ubuntu/Debian 38 | package 'postgresql-contrib15' 39 | 40 | # Install adminpack extension 41 | postgresql_extension 'postgres adminpack' do 42 | dbname 'postgres' 43 | extension 'adminpack' 44 | end 45 | ``` 46 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: ci 3 | "on": 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | lint-unit: 11 | uses: sous-chefs/.github/.github/workflows/lint-unit.yml@5.0.8 12 | permissions: 13 | actions: write 14 | checks: write 15 | pull-requests: write 16 | statuses: write 17 | issues: write 18 | 19 | integration: 20 | needs: lint-unit 21 | runs-on: ubuntu-latest 22 | strategy: 23 | matrix: 24 | os: 25 | - "almalinux-9" 26 | - "rockylinux-9" 27 | - "oraclelinux-9" 28 | - "centos-stream-9" 29 | - "centos-stream-10" 30 | - "amazonlinux-2023" 31 | - "debian-12" 32 | - "debian-13" 33 | - "ubuntu-2204" 34 | - "ubuntu-2404" 35 | suite: 36 | - "access-15" 37 | - "access-16" 38 | - "access-17" 39 | - "client-install-15" 40 | - "client-install-16" 41 | - "client-install-17" 42 | - "extension-15" 43 | - "extension-16" 44 | - "extension-17" 45 | - "ident-15" 46 | - "ident-16" 47 | - "ident-17" 48 | - "server-install-15" 49 | - "server-install-16" 50 | - "server-install-17" 51 | - "initdb-locale-17" 52 | - "server-install-os" 53 | exclude: 54 | - os: "amazonlinux-2023" 55 | suite: "access-15" 56 | - os: "amazonlinux-2023" 57 | suite: "access-16" 58 | - os: "amazonlinux-2023" 59 | suite: "access-17" 60 | - os: "amazonlinux-2023" 61 | suite: "client-install-15" 62 | - os: "amazonlinux-2023" 63 | suite: "client-install-16" 64 | - os: "amazonlinux-2023" 65 | suite: "client-install-17" 66 | - os: "amazonlinux-2023" 67 | suite: "extension-15" 68 | - os: "amazonlinux-2023" 69 | suite: "extension-16" 70 | - os: "amazonlinux-2023" 71 | suite: "extension-17" 72 | - os: "amazonlinux-2023" 73 | suite: "ident-15" 74 | - os: "amazonlinux-2023" 75 | suite: "ident-16" 76 | - os: "amazonlinux-2023" 77 | suite: "ident-17" 78 | - os: "amazonlinux-2023" 79 | suite: "server-install-15" 80 | - os: "amazonlinux-2023" 81 | suite: "server-install-16" 82 | - os: "amazonlinux-2023" 83 | suite: "server-install-17" 84 | - os: "amazonlinux-2023" 85 | suite: "initdb-locale-17" 86 | fail-fast: false 87 | 88 | steps: 89 | - name: Check out code 90 | uses: actions/checkout@v5 91 | - name: Install Chef 92 | uses: actionshub/chef-install@main 93 | - name: Dokken 94 | uses: actionshub/test-kitchen@main 95 | env: 96 | CHEF_LICENSE: accept-no-persist 97 | KITCHEN_LOCAL_YAML: kitchen.dokken.yml 98 | with: 99 | suite: ${{ matrix.suite }} 100 | os: ${{ matrix.os }} 101 | -------------------------------------------------------------------------------- /documentation/postgresql_access.md: -------------------------------------------------------------------------------- 1 | # postgresql_access 2 | 3 | [Back to resource list](../README.md#resources) 4 | 5 | This resource uses the initialised accumulator pattern to manage the `pg_hba.conf` file. It fully supports `load_current_value` and will report changes during the run and fire notifications. 6 | The content of `pg_hba.conf` is loaded into the template variables upon the first call of the `:postgresql_access` resource, so, to remove an entry from the file the resource must be called with the `:delete` action. 7 | 8 | ## Uses 9 | 10 | - [partial/_config_file](partial/_config_file.md) 11 | 12 | ## Actions 13 | 14 | - `:create` - Create an access entry 15 | - `:update` - Update a pre-existing access entry 16 | - `:delete` - Remove an access entry 17 | - `:grant` - Alias of `:create` 18 | 19 | ## Properties 20 | 21 | | Name | Name? | Type | Default | Description | Allowed Values | 22 | | -------------- | ----- | ------------ | ------- | ----------------------------------------------------------------------------- | -------------- | 23 | | `config_file` | | String | | | | 24 | | `source` | | String | | | | 25 | | `type` | | String | | Access record type | | 26 | | `database` | | String | | Access record database | | 27 | | `user` | | String | | Access record user | | 28 | | `address` | | String | | Access record address | | 29 | | `auth_method` | | String | | Access record authentication method | | 30 | | `auth_options` | | String, Hash | | Access record authentication options | | 31 | | `comment` | | String | | Access record comment | | 32 | | `position` | | Integer | | Access record order in file, empty spaces between positions will be truncated | | 33 | 34 | ## Libraries 35 | 36 | - `PostgreSQL::Cookbook::AccessHelpers::PgHbaTemplate` 37 | 38 | ## Examples 39 | 40 | To grant access to the PostgreSQL user with ident authentication: 41 | 42 | ```ruby 43 | postgresql_access `local_postgres_superuser` do 44 | comment `Local postgres superuser access` 45 | type `local` 46 | database `all` 47 | user `postgres` 48 | auth_method `ident` 49 | end 50 | ``` 51 | 52 | This generates the following line in the `pg_hba.conf`: 53 | 54 | ```config 55 | # Local postgres superuser access 56 | local all postgres ident # Local postgres superuser access 57 | ``` 58 | -------------------------------------------------------------------------------- /TESTING.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | 3 | Please refer to [the community cookbook documentation on testing](https://github.com/chef-cookbooks/community_cookbook_documentation/blob/main/TESTING.MD). 4 | 5 | ## Quick Start for Local Testing 6 | 7 | ### Prerequisites 8 | 9 | - **Chef Workstation**: Install from [Chef Downloads](https://www.chef.io/downloads/tools/workstation) 10 | - **Docker**: Required for Dokken driver (faster local testing) 11 | - macOS: [Docker Desktop](https://www.docker.com/products/docker-desktop) 12 | - Linux: Install via package manager 13 | 14 | ### Setup 15 | 16 | 1. **Enable Dokken driver** (faster than Vagrant): 17 | 18 | ```bash 19 | export KITCHEN_LOCAL_YAML=kitchen.dokken.yml 20 | ``` 21 | 22 | Or add to your shell profile (`~/.bashrc`, `~/.zshrc`, or use `mise.toml`): 23 | 24 | ```bash 25 | echo 'export KITCHEN_LOCAL_YAML=kitchen.dokken.yml' >> ~/.zshrc 26 | ``` 27 | 28 | 2. **Verify setup**: 29 | 30 | ```bash 31 | kitchen list 32 | ``` 33 | 34 | You should see Dokken as the driver for all instances. 35 | 36 | ### Running Tests 37 | 38 | #### Run a single suite on one platform 39 | 40 | ```bash 41 | kitchen test ident-16-debian-12 42 | ``` 43 | 44 | #### Run all platforms for a suite 45 | 46 | ```bash 47 | kitchen test ident-16 48 | ``` 49 | 50 | #### Run specific suite on multiple platforms for verification 51 | 52 | ```bash 53 | kitchen test ident-16-debian-12 ident-16-ubuntu-2204 ident-16-rockylinux-9 54 | ``` 55 | 56 | #### Debug a failing test 57 | 58 | ```bash 59 | # Create and converge the instance 60 | kitchen converge ident-16-debian-12 61 | 62 | # Login to inspect 63 | kitchen login ident-16-debian-12 64 | 65 | # Inside the container, check PostgreSQL status 66 | systemctl status postgresql-16 67 | cat /var/lib/pgsql/16/data/pg_ident.conf 68 | cat /var/lib/pgsql/16/data/pg_hba.conf 69 | tail -f /var/lib/pgsql/16/data/log/postgresql-*.log 70 | 71 | # Run tests manually 72 | kitchen verify ident-16-debian-12 73 | 74 | # Cleanup when done 75 | kitchen destroy ident-16-debian-12 76 | ``` 77 | 78 | ### Troubleshooting 79 | 80 | #### Docker permission errors 81 | 82 | ```bash 83 | # Linux: Add your user to docker group 84 | sudo usermod -aG docker $USER 85 | # Then logout and login again 86 | ``` 87 | 88 | #### Kitchen hangs or fails to start 89 | 90 | ```bash 91 | # Clean up old containers 92 | docker ps -a | grep kitchen | awk '{print $1}' | xargs docker rm -f 93 | 94 | # Clean up dokken network 95 | docker network prune 96 | ``` 97 | 98 | #### Tests pass locally but fail in CI 99 | 100 | - Ensure you're using the same PostgreSQL version (check `node['test']['pg_ver']`) 101 | - Check platform differences (RHEL vs Debian package names, paths) 102 | - Review CI logs for specific error messages 103 | 104 | ### Test Suite Overview 105 | 106 | - **access-\***: Tests `postgresql_access` resource (pg_hba.conf management) 107 | - **client-install-\***: Tests client-only installation 108 | - **extension-\***: Tests PostgreSQL extension installation 109 | - **ident-\***: Tests `postgresql_ident` resource (pg_ident.conf management) 110 | - **initdb-locale-\***: Tests database initialization with custom locale 111 | - **server-install-\***: Tests full server installation 112 | - **all-repos-install-\***: Tests installation with all repository options enabled 113 | - **no-repos-install-\***: Tests installation without PGDG repositories 114 | - **repo-\***: Tests repository configuration only 115 | -------------------------------------------------------------------------------- /test/cookbooks/test/recipes/access.rb: -------------------------------------------------------------------------------- 1 | postgresql_install 'postgresql' do 2 | version node['test']['pg_ver'] 3 | 4 | action %i(install init_server) 5 | end 6 | 7 | postgresql_access 'postgresql host superuser' do 8 | type 'host' 9 | database 'all' 10 | user 'postgres' 11 | address '127.0.0.1/32' 12 | auth_method 'scram-sha-256' 13 | end 14 | 15 | postgresql_service 'postgresql' do 16 | action %i(enable start) 17 | end 18 | 19 | postgresql_user 'postgres' do 20 | unencrypted_password '12345' 21 | action :nothing 22 | end 23 | 24 | postgresql_user 'sous_chef' do 25 | unencrypted_password '12345' 26 | 27 | notifies :set_password, 'postgresql_user[postgres]', :immediately 28 | end 29 | 30 | postgresql_user 'sous_chef' do 31 | superuser true 32 | unencrypted_password '67890' 33 | config({ statement_timeout: '8min' }) 34 | login true 35 | sensitive false 36 | action :update 37 | end 38 | 39 | postgresql_database 'sous_chef' do 40 | template 'template0' 41 | encoding 'utf8' 42 | end 43 | 44 | postgresql_access 'a sous_chef local superuser' do 45 | type 'host' 46 | database 'all' 47 | user 'sous_chef' 48 | auth_method 'scram-sha-256' 49 | address '127.0.0.1/32' 50 | position 5 51 | 52 | notifies :restart, 'postgresql_service[postgresql]', :delayed 53 | end 54 | 55 | postgresql_user 'name-with-dash' do 56 | unencrypted_password '1234' 57 | end 58 | 59 | postgresql_user 'name-with-dash' do 60 | config({ statement_timeout: '8min' }) 61 | sensitive false 62 | action :update 63 | end 64 | 65 | postgresql_user 'dropable-user' do 66 | unencrypted_password '1234' 67 | action [:create, :drop] 68 | not_if { ::File.exist?('/tmp/dropable-user.txt') } 69 | end 70 | 71 | file '/tmp/dropable-user.txt' 72 | 73 | postgresql_access 'access with hostname address' do 74 | type 'host' 75 | database 'all' 76 | user 'hostname_user' 77 | auth_method 'scram-sha-256' 78 | address 'host.domain' 79 | 80 | notifies :restart, 'postgresql_service[postgresql]', :delayed 81 | end 82 | 83 | postgresql_access 'access with hostname address username with dot' do 84 | type 'host' 85 | database 'all' 86 | user 'hostname.user' 87 | auth_method 'scram-sha-256' 88 | address 'host.domain' 89 | 90 | notifies :restart, 'postgresql_service[postgresql]', :delayed 91 | end 92 | 93 | postgresql_access 'access with database name with an underscore' do 94 | type 'host' 95 | database 'my_database' 96 | user 'hostname.user' 97 | auth_method 'scram-sha-256' 98 | address 'host.domain' 99 | 100 | notifies :restart, 'postgresql_service[postgresql]', :delayed 101 | end 102 | 103 | postgresql_access 'access with hostname long' do 104 | type 'host' 105 | database 'my_database' 106 | user 'hostname.user' 107 | auth_method 'scram-sha-256' 108 | address 'a.very.long.host.domain.that.exceeds.the.max.of.24.characters' 109 | 110 | notifies :restart, 'postgresql_service[postgresql]', :delayed 111 | end 112 | 113 | postgresql_access 'access with urls as auth_options' do 114 | type 'host' 115 | database 'all' 116 | user 'ldap_url.user' 117 | address '127.0.0.1/32' 118 | auth_method 'ldap' 119 | auth_options 'ldapurl="ldap://ldap.example.net/dc=example,dc=net?uid?sub"' 120 | end 121 | 122 | postgresql_access 'access with several auth_options' do 123 | type 'host' 124 | database 'all' 125 | user 'ldap_options.user' 126 | address '127.0.0.1/32' 127 | auth_method 'ldap' 128 | auth_options ldapserver: 'ldap.example.net', 129 | ldapbasedn: '"dc=example, dc=net"', 130 | ldapsearchattribute: 'uid' 131 | end 132 | 133 | postgresql_access 'access with multiple databases' do 134 | type 'host' 135 | database 'foo,bar' 136 | user 'john,doe' 137 | address '127.0.0.1/32' 138 | auth_method 'scram-sha-256' 139 | end 140 | -------------------------------------------------------------------------------- /resources/ident.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Cookbook:: postgresql 3 | # Resource:: ident 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | unified_mode true 19 | 20 | use 'partial/_config_file' 21 | 22 | property :config_file, String, 23 | default: lazy { ::File.join(conf_dir, 'pg_ident.conf') } 24 | 25 | property :source, String, 26 | default: 'pg_ident.conf.erb' 27 | 28 | property :map_name, String, 29 | name_property: true, 30 | description: 'Arbitrary name that will be used to refer to this mapping in pg_hba.conf' 31 | 32 | property :system_username, String, 33 | required: true, 34 | description: 'Operating system user name ' 35 | 36 | property :database_username, String, 37 | required: true, 38 | description: 'Database user name ' 39 | 40 | property :comment, String, 41 | coerce: proc { |p| p.start_with?('#') ? p : "# #{p}" }, 42 | description: 'Ident mapping record comment' 43 | 44 | load_current_value do |new_resource| 45 | current_value_does_not_exist! unless ::File.exist?(new_resource.config_file) 46 | 47 | if ::File.exist?(new_resource.config_file) 48 | owner ::Etc.getpwuid(::File.stat(new_resource.config_file).uid).name 49 | group ::Etc.getgrgid(::File.stat(new_resource.config_file).gid).name 50 | filemode ::File.stat(new_resource.config_file).mode.to_s(8)[-4..-1] 51 | end 52 | 53 | ident_file = PostgreSQL::Cookbook::IdentHelpers::PgIdent::PgIdentFile.read(new_resource.config_file) 54 | 55 | current_value_does_not_exist! unless ident_file.entry?(new_resource.map_name) 56 | 57 | entry = ident_file.entry(new_resource.map_name) 58 | %i(map_name system_username database_username comment).each { |p| send(p, entry.send(p)) } 59 | end 60 | 61 | action_class do 62 | include PostgreSQL::Cookbook::IdentHelpers::PgIdentTemplate 63 | end 64 | 65 | action :create do 66 | converge_if_changed do 67 | config_resource_init 68 | entry = config_resource.variables[:pg_ident].entry(new_resource.map_name) 69 | 70 | if nil_or_empty?(entry) 71 | resource_properties = %i(map_name system_username database_username comment).map { |p| [ p, new_resource.send(p) ] }.to_h.compact 72 | entry = PostgreSQL::Cookbook::IdentHelpers::PgIdent::PgIdentFileEntry.new(**resource_properties) 73 | config_resource.variables[:pg_ident].add(entry) 74 | else 75 | entry.update(system_username: new_resource.system_username, database_username: new_resource.database_username, comment: new_resource.comment) 76 | end 77 | end 78 | end 79 | 80 | action :update do 81 | converge_if_changed(:system_username, :database_username, :comment) do 82 | config_resource_init 83 | entry = config_resource.variables[:pg_ident].entry(new_resource.map_name) 84 | 85 | raise Chef::Exceptions::CurrentValueDoesNotExist, "Cannot update ident entry for '#{new_resource.map_name}' as it does not exist" if nil_or_empty?(entry) 86 | 87 | entry.update(system_username: new_resource.system_username, database_username: new_resource.database_username, comment: new_resource.comment) 88 | end 89 | end 90 | 91 | action :delete do 92 | config_resource_init 93 | 94 | converge_by("Remove ident entry with map_name: #{new_resource.map_name}") do 95 | config_resource.variables[:pg_ident].remove(new_resource.map_name) 96 | end if config_resource.variables[:pg_ident].entry?(new_resource.map_name) 97 | end 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PostgreSQL cookbook 2 | 3 | [![Cookbook Version](https://img.shields.io/cookbook/v/postgresql.svg)](https://supermarket.chef.io/cookbooks/postgresql) 4 | [![Build Status](https://img.shields.io/circleci/project/github/sous-chefs/postgresql/master.svg)](https://circleci.com/gh/sous-chefs/postgresql) 5 | [![OpenCollective](https://opencollective.com/sous-chefs/backers/badge.svg)](#backers) 6 | [![OpenCollective](https://opencollective.com/sous-chefs/sponsors/badge.svg)](#sponsors) 7 | [![License](https://img.shields.io/badge/License-Apache%202.0-green.svg)](https://opensource.org/licenses/Apache-2.0) 8 | 9 | Installs and configures PostgreSQL as a client or a server. 10 | 11 | ## Maintainers 12 | 13 | This cookbook is maintained by the Sous Chefs. The Sous Chefs are a community of Chef cookbook maintainers working together to maintain important cookbooks. If you’d like to know more please visit [sous-chefs.org](https://sous-chefs.org/) or come chat with us on the Chef Community Slack in [#sous-chefs](https://chefcommunity.slack.com/messages/C2V7B88SF). 14 | 15 | ## Upgrading 16 | 17 | If you are wondering where all the recipes went in v7.0+, or how on earth I use this new cookbook please see UPGRADING.md for a full description. 18 | 19 | ## Requirements 20 | 21 | ### Platforms 22 | 23 | - Amazon Linux 24 | - Debian 9+ 25 | - Ubuntu 18.04+ 26 | - Red Hat/CentOS/Scientific 7+ 27 | 28 | ### PostgreSQL version 29 | 30 | We follow the currently supported versions listed on 31 | 32 | ### Chef 33 | 34 | - Chef 18 35 | 36 | ### Cookbook Dependencies 37 | 38 | - `yum` >= 7.2.0 (for `dnf_module` resource) 39 | 40 | ## Resources 41 | 42 | - [postgresql_access](documentation/postgresql_access.md) 43 | - [postgresql_config](documentation/postgresql_config.md) 44 | - [postgresql_database](documentation/postgresql_database.md) 45 | - [postgresql_extension](documentation/postgresql_extension.md) 46 | - [postgresql_ident](documentation/postgresql_ident.md) 47 | - [postgresql_install](documentation/postgresql_install.md) 48 | - [postgresql_role](documentation/postgresql_role.md) 49 | - [postgresql_service](documentation/postgresql_service.md) 50 | 51 | ## Contributors 52 | 53 | This project exists thanks to all the people who [contribute.](https://opencollective.com/sous-chefs/contributors.svg?width=890&button=false) 54 | 55 | ### Backers 56 | 57 | Thank you to all our backers! 58 | 59 | ![https://opencollective.com/sous-chefs#backers](https://opencollective.com/sous-chefs/backers.svg?width=600&avatarHeight=40) 60 | 61 | ### Sponsors 62 | 63 | Support this project by becoming a sponsor. Your logo will show up here with a link to your website. 64 | 65 | ![https://opencollective.com/sous-chefs/sponsor/0/website](https://opencollective.com/sous-chefs/sponsor/0/avatar.svg?avatarHeight=100) 66 | ![https://opencollective.com/sous-chefs/sponsor/1/website](https://opencollective.com/sous-chefs/sponsor/1/avatar.svg?avatarHeight=100) 67 | ![https://opencollective.com/sous-chefs/sponsor/2/website](https://opencollective.com/sous-chefs/sponsor/2/avatar.svg?avatarHeight=100) 68 | ![https://opencollective.com/sous-chefs/sponsor/3/website](https://opencollective.com/sous-chefs/sponsor/3/avatar.svg?avatarHeight=100) 69 | ![https://opencollective.com/sous-chefs/sponsor/4/website](https://opencollective.com/sous-chefs/sponsor/4/avatar.svg?avatarHeight=100) 70 | ![https://opencollective.com/sous-chefs/sponsor/5/website](https://opencollective.com/sous-chefs/sponsor/5/avatar.svg?avatarHeight=100) 71 | ![https://opencollective.com/sous-chefs/sponsor/6/website](https://opencollective.com/sous-chefs/sponsor/6/avatar.svg?avatarHeight=100) 72 | ![https://opencollective.com/sous-chefs/sponsor/7/website](https://opencollective.com/sous-chefs/sponsor/7/avatar.svg?avatarHeight=100) 73 | ![https://opencollective.com/sous-chefs/sponsor/8/website](https://opencollective.com/sous-chefs/sponsor/8/avatar.svg?avatarHeight=100) 74 | ![https://opencollective.com/sous-chefs/sponsor/9/website](https://opencollective.com/sous-chefs/sponsor/9/avatar.svg?avatarHeight=100) 75 | -------------------------------------------------------------------------------- /documentation/postgresql_install.md: -------------------------------------------------------------------------------- 1 | # postgresql_install 2 | 3 | [Back to resource list](../README.md#resources) 4 | 5 | ## Actions 6 | 7 | - `:install` 8 | - `:install_client` 9 | - `:install_server` 10 | - `:remove` 11 | - `:remove_client` 12 | - `:remove_server` 13 | - `:repository` 14 | - `:repository_create` 15 | - `:repository_delete` 16 | - `:init_server` 17 | 18 | ## Properties 19 | 20 | | Name | Name? | Type | Default | Description | Allowed Values | 21 | | ---------------------------------- | ----- | --------------- | ----------------- | ------------------------------------------------ | -------------- | 22 | | `sensitive` | | true, false | `true` | | | 23 | | `version` | | String, Integer | `'17'` | Version to install | | 24 | | `source` | | String, Symbol | `:repo` | Installation source | repo, os | 25 | | `client_packages` | | String, Array | platform specific | Client packages to install | | 26 | | `server_packages` | | String, Array | platform specific | Server packages to install | | 27 | | `repo_pgdg` | | true, false | `true` | Create pgdg repo | | 28 | | `setup_repo_pgdg` | | true, false | value of previous | Whether or not to manage the pgdg repo | | 29 | | `repo_pgdg_common` | | true, false | `true` | Create pgdg-common repo | | 30 | | `setup_repo_pgdg_common` | | true, false | value of previous | Whether or not to manage the pgdg_common repo | | 31 | | `repo_pgdg_source` | | true, false | `false` | Create pgdg-source repo | | 32 | | `setup_repo_pgdg_source` | | true, false | value of previous | Whether or not to manage the pgdg_source repo | | 33 | | `repo_pgdg_updates_testing` | | true, false | `false` | Create pgdg-updates-testing repo | | 34 | | `setup_repo_pgdg_updates_testing` | | true, false | value of previous | Whether or not to manage the pgdg_updates_testing repo | | 35 | | `repo_pgdg_source_updates_testing` | | true, false | `false` | Create pgdg-source-updates-testing repo | | 36 | | `setup_repo_pgdg_source_updates_testing` | | true, false | value of previous | Whether or not to manage the pgdg_source_updates_testing repo | | 37 | | `yum_gpg_key_uri` | | String | platform specific | YUM/DNF GPG key URL | | 38 | | `apt_repository_uri` | | String | [https://download.postgresql.org/pub/repos/apt/](https://download.postgresql.org/pub/repos/apt/) | apt repository URL | | 39 | | `apt_gpg_key_uri` | | String | [https://download.postgresql.org/pub/repos/apt/ACCC4CF8.asc](https://download.postgresql.org/pub/repos/apt/ACCC4CF8.asc) | apt GPG key URL | | 40 | | `initdb_additional_options` | | String | | Additional options to pass to the initdb command | | 41 | | `initdb_locale` | | String | | Locale to use for the initdb command | | 42 | | `initdb_encoding` | | String | | Encoding to use for the initdb command | | 43 | | `initdb_user` | | String | `'postgres'` | User to run the initdb command as | | 44 | 45 | ## Libraries 46 | 47 | - `PostgreSQL::Cookbook::Helpers` 48 | -------------------------------------------------------------------------------- /libraries/sql/database.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Cookbook:: postgresql 3 | # Library:: sql/database 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | require_relative '../_utils' 19 | require_relative '_connection' 20 | require_relative '_utils' 21 | 22 | module PostgreSQL 23 | module Cookbook 24 | module SqlHelpers 25 | module Database 26 | private 27 | 28 | include PostgreSQL::Cookbook::Utils 29 | include PostgreSQL::Cookbook::SqlHelpers::Connection 30 | include Utils 31 | 32 | def pg_database(name) 33 | sql = 'SELECT * from pg_database WHERE datname=$1' 34 | params = [ name ] 35 | database = execute_sql_params(sql, params, max_one_result: true) 36 | 37 | return if database.to_a.empty? 38 | 39 | database = database.to_a.pop 40 | map_pg_values!(database) 41 | 42 | database 43 | end 44 | 45 | def pg_database?(name) 46 | !nil_or_empty?(pg_database(name)) 47 | end 48 | 49 | def database_sql(new_resource) 50 | sql = [] 51 | sql.push("DATABASE \"#{new_resource.database}\"") 52 | 53 | properties = case new_resource.action.pop 54 | when :create 55 | %i( 56 | owner 57 | template 58 | encoding 59 | strategy 60 | locale 61 | lc_collate 62 | lc_ctype 63 | icu_locale 64 | locale_provider 65 | collation_version 66 | tablespace 67 | allow_connections 68 | connection_limit 69 | is_template 70 | ) 71 | when :update 72 | %i( 73 | allow_connections 74 | connection_limit 75 | is_template 76 | ) 77 | end 78 | 79 | if properties.any? { |p| property_is_set?(p) } 80 | sql.push('WITH') 81 | 82 | properties.each do |p| 83 | next if nil_or_empty?(new_resource.send(p)) 84 | 85 | property_string = if %i(allow_connections connection_limit is_template).include?(p) || p.is_a?(Integer) 86 | "#{p.to_s.upcase}=#{new_resource.send(p)}" 87 | else 88 | "#{p.to_s.upcase}=\"#{new_resource.send(p)}\"" 89 | end 90 | 91 | sql.push(property_string) 92 | end 93 | end 94 | 95 | "#{sql.join(' ').strip};" 96 | end 97 | 98 | def create_database(new_resource) 99 | execute_sql("CREATE #{database_sql(new_resource)}") 100 | end 101 | 102 | def update_database(new_resource) 103 | execute_sql("ALTER #{database_sql(new_resource)}") 104 | end 105 | 106 | def update_database_owner(new_resource) 107 | execute_sql("ALTER DATABASE #{new_resource.database} OWNER TO #{new_resource.owner}") 108 | end 109 | 110 | def update_database_tablespace(new_resource) 111 | execute_sql("ALTER DATABASE #{new_resource.database} SET TABLESPACE #{new_resource.tablespace}") 112 | end 113 | 114 | def drop_database(new_resource) 115 | sql = "DROP DATABASE #{new_resource.database}" 116 | sql.concat(' WITH FORCE') if new_resource.force 117 | execute_sql(sql) 118 | end 119 | end 120 | end 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /resources/config.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Cookbook:: postgresql 3 | # Resource:: config 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | unified_mode true 19 | 20 | require 'deepsort' 21 | 22 | use 'partial/_config_file' 23 | 24 | property :config_file, String, 25 | default: lazy { ::File.join(conf_dir, 'postgresql.conf') } 26 | 27 | property :source, String, 28 | default: 'postgresql.conf.erb' 29 | 30 | property :version, [String, Integer], 31 | default: lazy { installed_postgresql_major_version }, 32 | desired_state: false, 33 | coerce: proc(&:to_s), 34 | description: 'PostgreSQL installed version override' 35 | 36 | property :data_directory, String, 37 | default: lazy { data_dir }, 38 | description: 'PostgreSQL server data directory' 39 | 40 | property :hba_file, String, 41 | description: 'PostgreSQL pg_hba.conf file location' 42 | 43 | property :ident_file, String, 44 | description: 'PostgreSQL pg_ident.conf file location' 45 | 46 | property :external_pid_file, String, 47 | description: 'PostgreSQL external PID file location' 48 | 49 | property :server_config, Hash, 50 | default: {}, 51 | coerce: proc { |p| p.transform_keys(&:to_s).deep_sort }, 52 | description: 'PostgreSQL server configuration options' 53 | 54 | load_current_value do |new_resource| 55 | current_value_does_not_exist! unless ::File.exist?(new_resource.config_file) 56 | 57 | if ::File.exist?(new_resource.config_file) 58 | owner ::Etc.getpwuid(::File.stat(new_resource.config_file).uid).name 59 | group ::Etc.getgrgid(::File.stat(new_resource.config_file).gid).name 60 | filemode ::File.stat(new_resource.config_file).mode.to_s(8)[-4..-1] 61 | end 62 | 63 | postgresql_server_config = PostgreSQL::Cookbook::ConfigHelpers.postgresql_conf_load_file(new_resource.config_file).fetch('global').deep_sort 64 | postgresql_server_config.transform_values! { |v| v.is_a?(String) ? v.gsub("'", '') : v } 65 | 66 | %w(data_directory hba_file ident_file external_pid_file).each do |p| 67 | next unless postgresql_server_config.fetch(p, nil) 68 | 69 | send(p, postgresql_server_config.delete(p)) 70 | end 71 | 72 | server_config(postgresql_server_config) 73 | end 74 | 75 | action :create do 76 | converge_if_changed do 77 | config = { 78 | data_directory: new_resource.data_directory, 79 | hba_file: new_resource.hba_file, 80 | ident_file: new_resource.ident_file, 81 | external_pid_file: new_resource.external_pid_file, 82 | } 83 | 84 | config.merge!(new_resource.server_config.dup) 85 | config.transform_keys!(&:to_s) 86 | config.transform_values! { |v| v.is_a?(String) ? "'#{v}'" : v } 87 | 88 | template new_resource.config_file do 89 | cookbook new_resource.cookbook 90 | source new_resource.source 91 | 92 | owner new_resource.owner 93 | group new_resource.group 94 | mode new_resource.filemode 95 | 96 | variables( 97 | config: 98 | ) 99 | 100 | action :create 101 | end 102 | 103 | service_dir = case installed_postgresql_package_source 104 | when :os 105 | '/etc/systemd/system/postgresql.service.d' 106 | when :repo 107 | "/etc/systemd/system/postgresql-#{new_resource.version}.service.d" 108 | else 109 | raise ArgumentError, "Unknown installation source: #{installed_postgresql_package_source}" 110 | end 111 | 112 | if new_resource.external_pid_file 113 | directory service_dir do 114 | owner 'root' 115 | group 'root' 116 | mode '0755' 117 | action :create 118 | end 119 | 120 | template "#{service_dir}/10-pid.conf" do 121 | cookbook new_resource.cookbook 122 | source 'systemd/10-pid.conf.erb' 123 | 124 | owner 'root' 125 | group 'root' 126 | mode '0644' 127 | 128 | variables( 129 | version: new_resource.version 130 | ) 131 | 132 | action :create 133 | end 134 | else 135 | directory(service_dir) { action(:delete) } 136 | file("#{service_dir}/10-pid.conf") { action(:delete) } 137 | end 138 | end 139 | end 140 | 141 | action :delete do 142 | file(new_resource.config_file) { action(:delete) } 143 | end 144 | -------------------------------------------------------------------------------- /.github/copilot-instructions.md: -------------------------------------------------------------------------------- 1 | # Copilot Instructions for Sous Chefs Cookbooks 2 | 3 | ## Repository Overview 4 | 5 | **Chef cookbook** for managing software installation and configuration. Part of the Sous Chefs cookbook ecosystem. 6 | 7 | **Key Facts:** Ruby-based, Chef >= 16 required, supports various OS platforms (check metadata.rb, kitchen.yml and .github/workflows/ci.yml for which platforms to specifically test) 8 | 9 | ## Project Structure 10 | 11 | **Critical Paths:** 12 | - `recipes/` - Chef recipes for cookbook functionality (if this is a recipe-driven cookbook) 13 | - `resources/` - Custom Chef resources with properties and actions (if this is a resource-driven cookbook) 14 | - `spec/` - ChefSpec unit tests 15 | - `test/integration/` - InSpec integration tests (tests all platforms supported) 16 | - `test/cookbooks/` or `test/fixtures/` - Example cookbooks used during testing that show good examples of custom resource usage 17 | - `attributes/` - Configuration for recipe driven cookbooks (not applicable to resource cookbooks) 18 | - `libraries/` - Library helpers to assist with the cookbook. May contain multiple files depending on complexity of the cookbook. 19 | - `templates/` - ERB templates that may be used in the cookbook 20 | - `files/` - files that may be used in the cookbook 21 | - `metadata.rb`, `Berksfile` - Cookbook metadata and dependencies 22 | 23 | ## Build and Test System 24 | 25 | ### Environment Setup 26 | **MANDATORY:** Install Chef Workstation first - provides chef, berks, cookstyle, kitchen tools. 27 | 28 | ### Essential Commands (strict order) 29 | ```bash 30 | berks install # Install dependencies (always first) 31 | cookstyle # Ruby/Chef linting 32 | yamllint . # YAML linting 33 | markdownlint-cli2 '**/*.md' # Markdown linting 34 | chef exec rspec # Unit tests (ChefSpec) 35 | # Integration tests will be done via the ci.yml action. Do not run these. Only check the action logs for issues after CI is done running. 36 | ``` 37 | 38 | ### Critical Testing Details 39 | - **Kitchen Matrix:** Multiple OS platforms × software versions (check kitchen.yml for specific combinations) 40 | - **Docker Required:** Integration tests use Dokken driver 41 | - **CI Environment:** Set `CHEF_LICENSE=accept-no-persist` 42 | - **Full CI Runtime:** 30+ minutes for complete matrix 43 | 44 | ### Common Issues and Solutions 45 | - **Always run `berks install` first** - most failures are dependency-related 46 | - **Docker must be running** for kitchen tests 47 | - **Chef Workstation required** - no workarounds, no alternatives 48 | - **Test data bags needed** (optional for some cookbooks) in `test/integration/data_bags/` for convergence 49 | 50 | ## Development Workflow 51 | 52 | ### Making Changes 53 | 1. Edit recipes/resources/attributes/templates/libraries 54 | 2. Update corresponding ChefSpec tests in `spec/` 55 | 3. Also update any InSpec tests under test/integration 56 | 4. Ensure cookstyle and rspec passes at least. You may run `cookstyle -a` to automatically fix issues if needed. 57 | 5. Also always update all documentation found in README.md and any files under documentation/* 58 | 6. **Always update CHANGELOG.md** (required by Dangerfile) - Make sure this conforms with the Sous Chefs changelog standards. 59 | 60 | ### Pull Request Requirements 61 | - **PR description >10 chars** (Danger enforced) 62 | - **CHANGELOG.md entry** for all code changes 63 | - **Version labels** (major/minor/patch) required 64 | - **All linters must pass** (cookstyle, yamllint, markdownlint) 65 | - **Test updates** needed for code changes >5 lines and parameter changes that affect the code logic 66 | 67 | ## Chef Cookbook Patterns 68 | 69 | ### Resource Development 70 | - Custom resources in `resources/` with properties and actions 71 | - Include comprehensive ChefSpec tests for all actions 72 | - Follow Chef resource DSL patterns 73 | 74 | ### Recipe Conventions 75 | - Use `include_recipe` for modularity 76 | - Handle platforms with `platform_family?` conditionals 77 | - Use encrypted data bags for secrets (passwords, SSL certs) 78 | - Leverage attributes for configuration with defaults 79 | 80 | ### Testing Approach 81 | - **ChefSpec (Unit):** Mock dependencies, test recipe logic in `spec/` 82 | - **InSpec (Integration):** Verify actual system state in `test/integration/inspec/` - InSpec files should contain proper inspec.yml and controls directories so that it could be used by other suites more easily. 83 | - One test file per recipe, use standard Chef testing patterns 84 | 85 | ## Trust These Instructions 86 | 87 | These instructions are validated for Sous Chefs cookbooks. **Do not search for build instructions** unless information here fails. 88 | 89 | **Error Resolution Checklist:** 90 | 1. Verify Chef Workstation installation 91 | 2. Confirm `berks install` completed successfully 92 | 3. Ensure Docker is running for integration tests 93 | 4. Check for missing test data dependencies 94 | 95 | The CI system uses these exact commands - following them matches CI behavior precisely. 96 | -------------------------------------------------------------------------------- /documentation/postgresql_role.md: -------------------------------------------------------------------------------- 1 | # postgresql_role 2 | 3 | [Back to resource list](../README.md#resources) 4 | 5 | ## Uses 6 | 7 | - [partial/_connection](partial/_connection.md) 8 | 9 | ## Provides 10 | 11 | - :postgresql_role 12 | 13 | ## Actions 14 | 15 | - `:create` 16 | - `:update` 17 | - `:drop` 18 | - `:delete` 19 | - `:set_password` 20 | 21 | ## Properties 22 | 23 | | Name | Name? | Type | Default | Description | Allowed Values | 24 | | ---------------------- | ----- | --------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | 25 | | `sensitive` | | true, false | | | | 26 | | `rolename` | ✓ | String | | The name of the new role | | 27 | | `superuser` | | true, false | | Determine whether the new role is a "superuser" who can override all access restrictions within the database | | 28 | | `createdb` | | true, false | | Define a role's ability to create databases | | 29 | | `createrole` | | true, false | | Determine whether a role will be permitted to create new roles (that is, execute CREATE ROLE) | | 30 | | `inherit` | | true, false | | Determine whether a role "inherits" the privileges of roles it is a member of | | 31 | | `login` | | true, false | | Determine whether a role is allowed to log in | | 32 | | `replication` | | true, false | | Determine whether a role is a replication role | | 33 | | `bypassrls` | | true, false | | Determine whether a role bypasses every row-level security (RLS) policy | | 34 | | `connection_limit` | | Integer, String | | If role can log in, this specifies how many concurrent connections the role can make | | 35 | | `unencrypted_password` | | String | | Sets the role password via a plain text string | | 36 | | `encrypted_password` | | String | | Sets the role password via a pre-encrypted string | | 37 | | `valid_until` | | String | | Sets a date and time after which the role password is no longer valid | | 38 | | `in_role` | | String, Array | | Lists one or more existing roles to which the new role will be immediately added as a new membe | | 39 | | `role` | | String, Array | | Lists one or more existing roles which are automatically added as members of the new role | | 40 | | `admin` | | String, Array | | Like ROLE, but the named roles are added to the new role WITH ADMIN OPTION, giving them the right to grant membership in this role to others | | 41 | | `config` | | Hash | | Role config values as a Hash | | 42 | 43 | ## Libraries 44 | 45 | - `PostgreSQL::Cookbook::SqlHelpers::Role` 46 | 47 | ## Examples 48 | 49 | Create a user `user1` with a password, with `createdb` and set an expiration date to 2018, Dec 21. 50 | 51 | ```ruby 52 | postgresql_role 'user1' do 53 | unencrypted_password 'UserP4ssword' 54 | createdb true 55 | valid_until '2018-12-31' 56 | end 57 | ``` 58 | 59 | Create a user `user1` with a password, with `createdb` and set an expiration date to 2018, Dec 21. 60 | 61 | ```ruby 62 | postgresql_role 'user1' do 63 | unencrypted_password 'UserP4ssword' 64 | createdb true 65 | valid_until '2018-12-31' 66 | end 67 | ``` 68 | -------------------------------------------------------------------------------- /libraries/sql/role.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Cookbook:: postgresql 3 | # Library:: sql 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | require_relative '../_utils' 19 | require_relative '_connection' 20 | require_relative '_utils' 21 | 22 | module PostgreSQL 23 | module Cookbook 24 | module SqlHelpers 25 | module Role 26 | private 27 | 28 | include PostgreSQL::Cookbook::Utils 29 | include PostgreSQL::Cookbook::SqlHelpers::Connection 30 | include Utils 31 | 32 | def pg_role(name) 33 | sql = 'SELECT * FROM pg_roles WHERE rolname=$1' 34 | role = execute_sql_params(sql, [ name ]) 35 | 36 | return if role.to_a.empty? 37 | 38 | role = role.to_a.pop 39 | map_pg_values!(role) 40 | 41 | role 42 | end 43 | 44 | def pg_role?(name) 45 | sql = 'SELECT rolname FROM pg_roles WHERE rolname=$1' 46 | role = execute_sql_params(sql, [ name ], max_one_result: true) 47 | 48 | !nil_or_empty?(role) 49 | end 50 | 51 | def pg_role_password?(name) 52 | sql = 'SELECT rolpassword from pg_roles WHERE rolname=$1 AND rolpassword IS NOT NULL' 53 | password = execute_sql_params(sql, [ name ], max_one_result: true) 54 | 55 | !nil_or_empty?(password) 56 | end 57 | 58 | def pg_role_encrypted_password(name) 59 | sql = 'SELECT rolpassword FROM pg_authid WHERE rolname=$1 AND rolpassword IS NOT NULL' 60 | authid = execute_sql_params(sql, [ name], max_one_result: true) 61 | 62 | authid&.to_a&.pop&.fetch('rolpassword') 63 | end 64 | 65 | def role_sql(new_resource) 66 | sql = [] 67 | 68 | sql.push("ROLE \"#{new_resource.rolename}\" WITH") 69 | 70 | %i(superuser createdb createrole inherit login replication bypassrls).each do |perm| 71 | next unless property_is_set?(perm) 72 | 73 | if new_resource.send(perm) 74 | sql.push(perm.to_s.upcase.gsub('_', ' ').to_s) 75 | else 76 | sql.push("NO#{perm.to_s.upcase.gsub('_', ' ')}") 77 | end 78 | end 79 | 80 | sql.push("CONNECTION LIMIT #{new_resource.connection_limit}") 81 | 82 | if new_resource.encrypted_password 83 | sql.push("ENCRYPTED PASSWORD '#{new_resource.encrypted_password}'") 84 | elsif new_resource.unencrypted_password 85 | sql.push("PASSWORD '#{new_resource.unencrypted_password}'") 86 | else 87 | sql.push('PASSWORD NULL') 88 | end 89 | 90 | sql.push("VALID UNTIL '#{new_resource.valid_until}'") if new_resource.valid_until 91 | 92 | unless new_resource.action.eql?(:update) 93 | sql.push("IN ROLE #{new_resource.in_role}") if new_resource.in_role 94 | sql.push("ROLE #{new_resource.role}") if new_resource.role 95 | sql.push("ADMIN #{new_resource.admin}") if new_resource.admin 96 | end 97 | 98 | "#{sql.join(' ').strip};" 99 | end 100 | 101 | def create_role(new_resource) 102 | execute_sql("CREATE #{role_sql(new_resource)}") 103 | end 104 | 105 | def set_role_configuration(new_resource) 106 | execute_sql("ALTER ROLE \"#{new_resource.rolename}\" RESET ALL;") 107 | new_resource.config.each { |k, v| execute_sql("ALTER ROLE \"#{new_resource.rolename}\" SET \"#{k}\" = \"#{v}\";") } 108 | end 109 | 110 | def update_role(new_resource) 111 | execute_sql("ALTER #{role_sql(new_resource)}") 112 | end 113 | 114 | def drop_role(new_resource) 115 | execute_sql("DROP ROLE \"#{new_resource.rolename}\"") 116 | end 117 | 118 | def update_role_password(new_resource) 119 | sql = [] 120 | 121 | sql.push("ALTER ROLE \"#{new_resource.rolename}\"") 122 | 123 | if new_resource.encrypted_password 124 | sql.push("ENCRYPTED PASSWORD '#{new_resource.encrypted_password}'") 125 | elsif new_resource.unencrypted_password 126 | sql.push("PASSWORD '#{new_resource.unencrypted_password}'") 127 | else 128 | sql.push('PASSWORD NULL') 129 | end 130 | 131 | execute_sql("#{sql.join(' ').strip};") 132 | end 133 | 134 | # def update_role_with_attributes_sql(new_resource, attr, value) 135 | # sql = %(ALTER ROLE \\"#{new_resource.create_role}\\" SET #{attr} = #{value}) 136 | # execute_sql(sql) 137 | # end 138 | end 139 | end 140 | end 141 | end 142 | -------------------------------------------------------------------------------- /resources/access.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Cookbook:: postgresql 3 | # Resource:: access 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | unified_mode true 19 | 20 | use 'partial/_config_file' 21 | 22 | property :config_file, String, 23 | default: lazy { ::File.join(conf_dir, 'pg_hba.conf') }, 24 | desired_state: false 25 | 26 | property :source, String, 27 | default: 'pg_hba.conf.erb', 28 | desired_state: false 29 | 30 | property :type, String, 31 | required: true, 32 | description: 'Access record type' 33 | 34 | property :database, String, 35 | required: true, 36 | description: 'Access record database' 37 | 38 | property :user, String, 39 | required: true, 40 | description: 'Access record user' 41 | 42 | property :address, String, 43 | description: 'Access record address' 44 | 45 | property :auth_method, String, 46 | required: true, 47 | description: 'Access record authentication method' 48 | 49 | property :auth_options, [String, Hash], 50 | coerce: proc { |v| sorted_auth_options_string(v) }, 51 | description: 'Access record authentication options' 52 | 53 | property :comment, String, 54 | coerce: proc { |p| p.start_with?('#') ? p : "# #{p}" }, 55 | description: 'Access record comment' 56 | 57 | property :position, Integer, 58 | description: 'Access record order in file, empty spaces between positions will be truncated' 59 | 60 | load_current_value do |new_resource| 61 | current_value_does_not_exist! unless ::File.exist?(new_resource.config_file) 62 | 63 | if ::File.exist?(new_resource.config_file) 64 | owner ::Etc.getpwuid(::File.stat(new_resource.config_file).uid).name 65 | group ::Etc.getgrgid(::File.stat(new_resource.config_file).gid).name 66 | filemode ::File.stat(new_resource.config_file).mode.to_s(8)[-4..-1] 67 | end 68 | 69 | pg_hba_file = PostgreSQL::Cookbook::AccessHelpers::PgHba::PgHbaFile.read(new_resource.config_file) 70 | entry = pg_hba_file.entry(new_resource.type, new_resource.database, new_resource.user, new_resource.address) 71 | 72 | current_value_does_not_exist! if nil_or_empty?(entry) 73 | 74 | %i(type database user address auth_method auth_options comment).each do |p| 75 | next unless entry.respond_to?(p) 76 | 77 | send(p, entry.send(p).to_s) 78 | end 79 | position entry.position if new_resource.position 80 | end 81 | 82 | action_class do 83 | include PostgreSQL::Cookbook::AccessHelpers::PgHbaTemplate 84 | end 85 | 86 | action :create do 87 | converge_if_changed do 88 | config_resource_init 89 | entry = config_resource.variables[:pg_hba].entry(new_resource.type, new_resource.database, new_resource.user, new_resource.address) 90 | 91 | if nil_or_empty?(entry) 92 | resource_properties = %i(type database user address auth_method auth_options comment).map { |p| [ p, new_resource.send(p) ] }.to_h.compact 93 | entry = PostgreSQL::Cookbook::AccessHelpers::PgHba::PgHbaFileEntry.create(**resource_properties) 94 | config_resource.variables[:pg_hba].add(entry, new_resource.position) 95 | else 96 | run_action(:update) 97 | end 98 | 99 | # Ensure config file is written before notifications are sent 100 | config_resource.run_action(:create) 101 | end 102 | end 103 | 104 | action :update do 105 | converge_if_changed(:auth_method, :auth_options, :comment, :position) do 106 | config_resource_init 107 | entry = config_resource.variables[:pg_hba].entry(new_resource.type, new_resource.database, new_resource.user, new_resource.address) 108 | 109 | raise Chef::Exceptions::CurrentValueDoesNotExist, "Cannot update access entry for '#{new_resource.name}' as it does not exist" if nil_or_empty?(entry) 110 | 111 | entry.update(auth_method: new_resource.auth_method, auth_options: new_resource.auth_options, comment: new_resource.comment) 112 | 113 | config_resource.variables[:pg_hba].move(entry, new_resource.position) if property_is_set?(:position) 114 | end 115 | end 116 | 117 | action :delete do 118 | config_resource_init 119 | 120 | resource_properties = %i(type database user address auth_method auth_options).map { |p| [ p, new_resource.send(p) ] }.to_h.compact 121 | entry = PostgreSQL::Cookbook::AccessHelpers::PgHba::PgHbaFileEntry.create(**resource_properties) 122 | 123 | converge_by("Remove grant entry for #{new_resource.type} | #{new_resource.database} | #{new_resource.user} | #{new_resource.auth_method}") do 124 | config_resource.variables[:pg_hba].remove(entry) 125 | end if config_resource.variables[:pg_hba].include?(entry) 126 | end 127 | 128 | action(:grant) { run_action(:create) } 129 | -------------------------------------------------------------------------------- /resources/database.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Cookbook:: postgresql 3 | # Resource:: database 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | unified_mode true 19 | 20 | use 'partial/_connection' 21 | 22 | property :database, String, 23 | name_property: true, 24 | description: 'The name of a database to create' 25 | 26 | property :owner, [String, Integer], 27 | description: 'The role name of the user who will own the new database, or DEFAULT to use the default (namely, the user executing the command). To create a database owned by another role, you must be a direct or indirect member of that role, or be a superuser.' 28 | 29 | property :template, String, 30 | default: 'template1', 31 | description: 'The name of the template from which to create the new database, or DEFAULT to use the default template (template1)' 32 | 33 | property :encoding, [Integer, String], 34 | description: 'Character set encoding to use in the new database' 35 | 36 | property :strategy, String, 37 | description: 'Strategy to be used in creating the new database' 38 | 39 | property :locale, String, 40 | description: 'This is a shortcut for setting LC_COLLATE and LC_CTYPE at once' 41 | 42 | property :lc_collate, String, 43 | description: 'Collation order (LC_COLLATE) to use in the new database. This affects the sort order applied to strings, e.g., in queries with ORDER BY, as well as the order used in indexes on text columns. The default is to use the collation order of the template database.' 44 | 45 | property :lc_ctype, String, 46 | description: 'Character classification (LC_CTYPE) to use in the new database. This affects the categorization of characters, e.g., lower, upper and digit. The default is to use the character classification of the template database.' 47 | 48 | property :icu_locale, String, 49 | description: 'Specifies the ICU locale ID if the ICU locale provider is used' 50 | 51 | property :locale_provider, String, 52 | description: 'Specifies the provider to use for the default collation in this database' 53 | 54 | property :collation_version, String, 55 | description: 'Specifies the collation version string to store with the database' 56 | 57 | property :tablespace, String, 58 | description: 'The name of the tablespace that will be associated with the new database' 59 | 60 | property :allow_connections, [true, false], 61 | default: true, 62 | description: 'If false then no one can connect to this database' 63 | 64 | property :connection_limit, [Integer, String], 65 | default: -1, 66 | coerce: proc(&:to_s), 67 | description: 'How many concurrent connections can be made to this database. -1 (the default) means no limit.' 68 | 69 | property :is_template, [true, false], 70 | description: 'If true, then this database can be cloned by any user with CREATEDB privileges; if false (the default), then only superusers or the owner of the database can clone it.' 71 | 72 | include PostgreSQL::Cookbook::SqlHelpers::Database 73 | 74 | load_current_value do |new_resource| 75 | current_value_does_not_exist! unless pg_database?(new_resource.database) 76 | 77 | database_data = pg_database(new_resource.database) 78 | 79 | database(database_data.fetch('datname', nil)) 80 | owner(database_data.fetch('datdba', nil)) 81 | encoding(database_data.fetch('encoding', nil).to_i) 82 | lc_collate(database_data.fetch('datcollate', nil)) 83 | lc_ctype(database_data.fetch('datctype', nil)) 84 | icu_locale(database_data.fetch('daticulocale', nil)) 85 | locale_provider(database_data.fetch('datlocprovider', nil)) 86 | tablespace(database_data.fetch('dattablespace', nil)) 87 | allow_connections(database_data.fetch('datallowconn', nil)) 88 | connection_limit(database_data.fetch('datconnlimit', nil)) 89 | is_template(database_data.fetch('datistemplate', nil)) 90 | end 91 | 92 | action_class do 93 | include PostgreSQL::Cookbook::SqlHelpers::Database 94 | end 95 | 96 | action :create do 97 | converge_if_changed { create_database(new_resource) } unless pg_database?(new_resource.database) 98 | end 99 | 100 | action :update do 101 | raise Chef::Exceptions::CurrentValueDoesNotExist, "Cannot update database '#{new_resource.database}' as it does not exist" unless pg_database?(new_resource.database) 102 | 103 | converge_if_changed(:allow_connections, :connection_limit, :is_template) do 104 | update_database(new_resource) 105 | end 106 | 107 | converge_if_changed(:owner) do 108 | update_database_owner(new_resource) 109 | end 110 | 111 | converge_if_changed(:tablespace) do 112 | update_database_tablespace(new_resource) 113 | end 114 | end 115 | 116 | action :drop do 117 | converge_by("Drop database #{new_resource.database}") { drop_database(new_resource) } if pg_database?(new_resource.database) 118 | end 119 | 120 | action :delete do 121 | run_action(:drop) 122 | end 123 | -------------------------------------------------------------------------------- /test/integration/access/controls/base_access.rb: -------------------------------------------------------------------------------- 1 | pg_superuser_session = postgres_session('postgres', '12345', '127.0.0.1') 2 | hba_conf_path = pg_superuser_session.query('SHOW hba_file;').output.strip 3 | 4 | control 'postgresql-local-access' do 5 | impact 1.0 6 | desc 'This test ensures postgres has localhost access to the database' 7 | 8 | describe postgres_hba_conf(hba_conf_path).where { type == 'host' && user == 'postgres' } do 9 | its('database') { should cmp 'all' } 10 | its('user') { should cmp 'postgres' } 11 | its('auth_method') { should cmp 'scram-sha-256' } 12 | its('address') { should cmp '127.0.0.1/32' } 13 | end 14 | 15 | describe pg_superuser_session.query('SELECT 1;') do 16 | its('output') { should eq '1' } 17 | end 18 | end 19 | 20 | control 'postgresql-sous-chef-access' do 21 | impact 1.0 22 | desc 'This test ensures sous_chefs have local trust access to the database' 23 | 24 | describe postgres_hba_conf(hba_conf_path).where { user == 'sous_chef' } do 25 | its('database') { should cmp 'all' } 26 | its('type') { should cmp 'host' } 27 | its('auth_method') { should cmp 'scram-sha-256' } 28 | its('address') { should cmp '127.0.0.1/32' } 29 | end 30 | 31 | postgres_access = postgres_session('sous_chef', '67890') 32 | 33 | describe postgres_access.query('SELECT 1;', ['postgres']) do 34 | its('output') { should eq '1' } 35 | end 36 | end 37 | 38 | control 'postgresql-hostname-access' do 39 | impact 1.0 40 | desc 'This test ensures hostnames may be specified in ACLs' 41 | 42 | describe postgres_hba_conf(hba_conf_path).where { user == 'hostname_user' } do 43 | its('database') { should cmp 'all' } 44 | its('type') { should cmp 'host' } 45 | its('auth_method') { should cmp 'scram-sha-256' } 46 | its('address') { should cmp 'host.domain' } 47 | end 48 | end 49 | 50 | control 'postgresql-long-hostname-access' do 51 | impact 1.0 52 | desc 'This test ensures long hostnames may be specified in ACLs' 53 | 54 | describe postgres_hba_conf(hba_conf_path).where { address == 'a.very.long.host.domain.that.exceeds.the.max.of.24.characters' } do 55 | its('database') { should cmp 'my_database' } 56 | its('type') { should cmp 'host' } 57 | its('auth_method') { should cmp 'scram-sha-256' } 58 | its('address') { should cmp 'a.very.long.host.domain.that.exceeds.the.max.of.24.characters' } 59 | end 60 | end 61 | 62 | control 'sous_chef role should exist' do 63 | impact 1.0 64 | desc 'The sous_chef database user role should exist' 65 | 66 | postgres_access = postgres_session('postgres', '12345', '127.0.0.1') 67 | 68 | describe postgres_access.query('SELECT rolname FROM pg_roles;') do 69 | its('output') { should cmp /sous_chef/ } 70 | end 71 | end 72 | 73 | control 'sous_chef has statement_timeout set to 8mins' do 74 | impact 1.0 75 | desc 'Ensures attributes are applied' 76 | 77 | postgres_access = postgres_session('sous_chef', '67890') 78 | 79 | describe postgres_access.query('SHOW statement_timeout;', ['postgres']) do 80 | its('output') { should cmp /8min/ } 81 | end 82 | end 83 | 84 | control 'name-with-dash role should exist' do 85 | impact 1.0 86 | desc 'The name-with-dash database user role should exist' 87 | 88 | postgres_access = postgres_session('postgres', '12345', '127.0.0.1') 89 | 90 | describe postgres_access.query('SELECT rolname FROM pg_roles;') do 91 | its('output') { should cmp /name-with-dash/ } 92 | end 93 | end 94 | 95 | control 'name-with-dash role has statement_timeout set to 8mins' do 96 | impact 1.0 97 | desc 'Ensures attributes are applied on users with dashes' 98 | 99 | postgres_access = postgres_session('postgres', '12345', '127.0.0.1') 100 | 101 | describe postgres_access.query("SELECT rolconfig FROM pg_roles where rolname = 'name-with-dash';") do 102 | its('output') { should cmp /statement_timeout=8min/ } 103 | end 104 | end 105 | 106 | control 'database sous_chef should exist with encoding UTF8' do 107 | impact 1.0 108 | desc 'Ensures database exists and encoding is correct' 109 | 110 | postgres_access = postgres_session('postgres', '12345', '127.0.0.1') 111 | 112 | describe postgres_access.query("SELECT encoding from pg_database WHERE datname='sous_chef';") do 113 | its('output') { should eql '6' } 114 | end 115 | end 116 | 117 | control 'postgresql-access-auth_options-with-url' do 118 | impact 1.0 119 | desc 'This test ensures URL may be specified in auth_options' 120 | 121 | describe postgres_hba_conf(hba_conf_path).where { user == 'ldap_url.user' } do 122 | its('type') { should cmp 'host' } 123 | its('database') { should cmp 'all' } 124 | its('user') { should cmp 'ldap_url.user' } 125 | its('address') { should cmp '127.0.0.1/32' } 126 | its('auth_method') { should cmp 'ldap' } 127 | its('auth_params') { should cmp 'ldapurl="ldap://ldap.example.net/dc=example,dc=net?uid?sub"' } 128 | end 129 | end 130 | 131 | control 'postgresql-access-multiple-auth_options' do 132 | impact 1.0 133 | desc 'This test ensures multiple auth_options may be specified' 134 | 135 | describe postgres_hba_conf(hba_conf_path).where { user == 'ldap_options.user' } do 136 | its('type') { should cmp 'host' } 137 | its('database') { should cmp 'all' } 138 | its('user') { should cmp 'ldap_options.user' } 139 | its('address') { should cmp '127.0.0.1/32' } 140 | its('auth_method') { should cmp 'ldap' } 141 | its('auth_params') { should cmp 'ldapbasedn="dc=example, dc=net" ldapsearchattribute=uid ldapserver=ldap.example.net' } 142 | end 143 | end 144 | -------------------------------------------------------------------------------- /spec/libraries/helpers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require_relative '../../libraries/helpers' 3 | 4 | RSpec.describe PostgreSQL::Cookbook::Helpers do 5 | class DummyClass < Chef::Node 6 | include PostgreSQL::Cookbook::Helpers 7 | end 8 | subject { DummyClass.new } 9 | 10 | %w(11 12 13 14 15).each do |pg_version| 11 | describe '#data_dir(version)' do 12 | before do 13 | allow(subject).to receive(:[]).with('platform_family').and_return(platform_family) 14 | end 15 | 16 | context "with rhel family and Postgres #{pg_version} from repo" do 17 | let(:platform_family) { 'rhel' } 18 | 19 | it 'returns the correct path' do 20 | expect(subject.data_dir(version: pg_version, source: :repo)).to eq "/var/lib/pgsql/#{pg_version}/data" 21 | end 22 | end 23 | 24 | context "with rhel family and Postgres #{pg_version} from os" do 25 | let(:platform_family) { 'rhel' } 26 | 27 | it 'returns the correct path' do 28 | expect(subject.data_dir(version: pg_version, source: :os)).to eq '/var/lib/pgsql/data' 29 | end 30 | end 31 | 32 | context "with debian family and Postgres #{pg_version} from repo" do 33 | let(:platform_family) { 'debian' } 34 | 35 | it 'returns the correct path' do 36 | expect(subject.data_dir(version: pg_version, source: :repo)).to eq "/var/lib/postgresql/#{pg_version}/main" 37 | end 38 | end 39 | 40 | context "with debian family and Postgres #{pg_version} from os" do 41 | let(:platform_family) { 'debian' } 42 | 43 | it 'returns the correct path' do 44 | expect(subject.data_dir(version: pg_version, source: :os)).to eq "/var/lib/postgresql/#{pg_version}/main" 45 | end 46 | end 47 | end 48 | 49 | describe '#conf_dir(version)' do 50 | before do 51 | allow(subject).to receive(:[]).with('platform_family').and_return(platform_family) 52 | end 53 | 54 | context "with rhel family and Postgres #{pg_version} from repo" do 55 | let(:platform_family) { 'rhel' } 56 | 57 | it 'returns the correct path' do 58 | expect(subject.conf_dir(version: pg_version, source: :repo)).to eq "/var/lib/pgsql/#{pg_version}/data" 59 | end 60 | end 61 | 62 | context "with rhel family and Postgres #{pg_version} from os" do 63 | let(:platform_family) { 'rhel' } 64 | 65 | it 'returns the correct path' do 66 | expect(subject.conf_dir(version: pg_version, source: :os)).to eq '/var/lib/pgsql/data' 67 | end 68 | end 69 | 70 | context "with debian family and Postgres #{pg_version} from repo" do 71 | let(:platform_family) { 'debian' } 72 | 73 | it 'returns the correct path' do 74 | expect(subject.conf_dir(version: pg_version, source: :repo)).to eq "/etc/postgresql/#{pg_version}/main" 75 | end 76 | end 77 | 78 | context "with debian family and Postgres #{pg_version} from os" do 79 | let(:platform_family) { 'debian' } 80 | 81 | it 'returns the correct path' do 82 | expect(subject.conf_dir(version: pg_version, source: :os)).to eq "/etc/postgresql/#{pg_version}/main" 83 | end 84 | end 85 | end 86 | 87 | describe '#default_platform_service_name(version)' do 88 | before do 89 | allow(subject).to receive(:[]).with(:platform_family).and_return(platform_family) 90 | end 91 | 92 | context "with rhel family and Postgres #{pg_version} from repo" do 93 | let(:platform_family) { 'rhel' } 94 | 95 | it 'returns the correct service name' do 96 | expect(subject.default_platform_service_name(version: pg_version, source: :repo)).to eq "postgresql-#{pg_version}" 97 | end 98 | end 99 | 100 | context "with rhel family and Postgres #{pg_version} from os" do 101 | let(:platform_family) { 'rhel' } 102 | 103 | it 'returns the correct service name' do 104 | expect(subject.default_platform_service_name(version: pg_version, source: :os)).to eq 'postgresql' 105 | end 106 | end 107 | 108 | context "with debian family and Postgres #{pg_version} from repo" do 109 | let(:platform_family) { 'debian' } 110 | 111 | it 'returns the correct service name' do 112 | expect(subject.default_platform_service_name(version: pg_version, source: :repo)).to eq 'postgresql' 113 | end 114 | end 115 | 116 | context "with debian family and Postgres #{pg_version} from os" do 117 | let(:platform_family) { 'debian' } 118 | 119 | it 'returns the correct service name' do 120 | expect(subject.default_platform_service_name(version: pg_version, source: :os)).to eq 'postgresql' 121 | end 122 | end 123 | end 124 | end 125 | 126 | describe '#dnf_module_platform?' do 127 | before do 128 | allow(subject).to receive(:platform_family?).and_return(false) 129 | allow(subject).to receive(:platform_family?).with('rhel').and_return(true) 130 | end 131 | 132 | it 'returns false on RHEL 7' do 133 | allow(subject).to receive(:[]).with('platform_version').and_return('7.9') 134 | 135 | expect(subject.dnf_module_platform?).to be false 136 | end 137 | 138 | it 'returns true on RHEL 8' do 139 | allow(subject).to receive(:[]).with('platform_version').and_return('8.9') 140 | 141 | expect(subject.dnf_module_platform?).to be true 142 | end 143 | 144 | it 'returns true on RHEL 9' do 145 | allow(subject).to receive(:[]).with('platform_version').and_return('9.4') 146 | 147 | expect(subject.dnf_module_platform?).to be true 148 | end 149 | 150 | it 'returns false on RHEL 10' do 151 | allow(subject).to receive(:[]).with('platform_version').and_return('10.0') 152 | 153 | expect(subject.dnf_module_platform?).to be false 154 | end 155 | end 156 | end 157 | -------------------------------------------------------------------------------- /documentation/postgresql_database.md: -------------------------------------------------------------------------------- 1 | # postgresql_database 2 | 3 | [Back to resource list](../README.md#resources) 4 | 5 | This resource manages PostgreSQL databases 6 | 7 | ## Uses 8 | 9 | - [partial/_connection](partial/_connection.md) 10 | 11 | ## Actions 12 | 13 | - `:create` - Creates the given database 14 | - `:update` - Updates the given database 15 | - `:drop` - Drops the given database 16 | - `:delete` - Alias for `:drop` 17 | 18 | ## Properties 19 | 20 | | Name | Name? | Type | Default | Description | Allowed Values | 21 | | ------------------- | ----- | --------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------- | 22 | | `database` | ✓ | String | | The name of a database to create | | 23 | | `owner` | | String, Integer | | The role name of the user who will own the new database, or DEFAULT to use the default (namely, the user executing the command). To create a database owned by another role, you must be a direct or indirect member of that role, or be a superuser. | | 24 | | `template` | | String | | The name of the template from which to create the new database, or DEFAULT to use the default template (template1) | | 25 | | `encoding` | | Integer | | Character set encoding to use in the new database | | 26 | | `strategy` | | String | | Strategy to be used in creating the new database | | 27 | | `locale` | | String | | This is a shortcut for setting LC_COLLATE and LC_CTYPE at once | | 28 | | `lc_collate` | | String | | Collation order (LC_COLLATE) to use in the new database. This affects the sort order applied to strings, e.g., in queries with ORDER BY, as well as the order used in indexes on text columns. The default is to use the collation order of the template database. | | 29 | | `lc_ctype` | | String | | Character classification (LC_CTYPE) to use in the new database. This affects the categorization of characters, e.g., lower, upper and digit. The default is to use the character classification of the template database. | | 30 | | `icu_locale` | | String | | Specifies the ICU locale ID if the ICU locale provider is used | | 31 | | `locale_provider` | | String | | Specifies the provider to use for the default collation in this database | | 32 | | `collation_version` | | String | | Specifies the collation version string to store with the database | | 33 | | `tablespace` | | String | | The name of the tablespace that will be associated with the new database | | 34 | | `allow_connections` | | true, false | | If false then no one can connect to this database | | 35 | | `connection_limit` | | Integer, String | | How many concurrent connections can be made to this database. -1 (the default) means no limit. | | 36 | | `is_template` | | true, false | | If true, then this database can be cloned by any user with CREATEDB privileges; if false (the default), then only superusers or the owner of the database can clone it. | | 37 | 38 | ## Libraries 39 | 40 | - `PostgreSQL::Cookbook::SqlHelpers::Database` 41 | 42 | ## Examples 43 | 44 | To create database named 'my_app' with owner 'user1': 45 | 46 | ```ruby 47 | postgresql_database 'my_app' do 48 | owner 'user1' 49 | end 50 | ``` 51 | -------------------------------------------------------------------------------- /resources/role.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Cookbook:: postgresql 3 | # Resource:: role 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | provides :postgresql_user 19 | unified_mode true 20 | 21 | use 'partial/_connection' 22 | 23 | property :sensitive, [true, false], 24 | default: true, 25 | desired_state: false 26 | 27 | property :rolename, String, 28 | name_property: true, 29 | description: 'The name of the new role' 30 | 31 | property :superuser, [true, false], 32 | description: 'Determine whether the new role is a "superuser" who can override all access restrictions within the database' 33 | 34 | property :createdb, [true, false], 35 | description: 'Define a role\'s ability to create databases' 36 | 37 | property :createrole, [true, false], 38 | description: 'Determine whether a role will be permitted to create new roles (that is, execute CREATE ROLE)' 39 | 40 | property :inherit, [true, false], 41 | description: 'Determine whether a role "inherits" the privileges of roles it is a member of' 42 | 43 | property :login, [true, false], 44 | description: 'Determine whether a role is allowed to log in' 45 | 46 | property :replication, [true, false], 47 | description: 'Determine whether a role is a replication role' 48 | 49 | property :bypassrls, [true, false], 50 | description: 'Determine whether a role bypasses every row-level security (RLS) policy' 51 | 52 | property :connection_limit, [Integer, String], 53 | default: -1, 54 | coerce: proc(&:to_s), 55 | description: 'If role can log in, this specifies how many concurrent connections the role can make' 56 | 57 | property :unencrypted_password, String, 58 | sensitive: true, 59 | description: 'Sets the role password via a plain text string' 60 | 61 | property :encrypted_password, String, 62 | description: 'Sets the role password via a pre-encrypted string' 63 | 64 | property :valid_until, String, 65 | description: 'Sets a date and time after which the role password is no longer valid' 66 | 67 | property :in_role, [String, Array], 68 | coerce: proc { |p| Array(p).join(', ') }, 69 | description: 'Lists one or more existing roles to which the new role will be immediately added as a new membe' 70 | 71 | property :role, [String, Array], 72 | coerce: proc { |p| Array(p).join(', ') }, 73 | description: 'Lists one or more existing roles which are automatically added as members of the new role' 74 | 75 | property :admin, [String, Array], 76 | coerce: proc { |p| Array(p).join(', ') }, 77 | description: 'Like ROLE, but the named roles are added to the new role WITH ADMIN OPTION, giving them the right to grant membership in this role to others' 78 | 79 | property :config, Hash, 80 | default: {}, 81 | coerce: proc { |p| p.transform_keys(&:to_s) }, 82 | description: 'Role config values as a Hash' 83 | 84 | include PostgreSQL::Cookbook::SqlHelpers::Role 85 | 86 | load_current_value do |new_resource| 87 | current_value_does_not_exist! unless pg_role?(new_resource.rolename) 88 | 89 | role_data = pg_role(new_resource.rolename) 90 | 91 | rolename(role_data.fetch('rolname', nil)) 92 | superuser(role_data.fetch('rolsuper', nil)) 93 | createdb(role_data.fetch('rolcreatedb', nil)) 94 | createrole(role_data.fetch('rolcreaterole', nil)) 95 | inherit(role_data.fetch('rolinherit', nil)) 96 | login(role_data.fetch('rolcanlogin', nil)) 97 | replication(role_data.fetch('rolreplication', nil)) 98 | bypassrls(role_data.fetch('rolbypassrls', nil)) 99 | connection_limit(role_data.fetch('rolconnlimit', nil)) 100 | valid_until(role_data.fetch('rolvaluntil', nil)) 101 | config(config_string_to_hash(role_data.fetch('rolconfig', nil))) 102 | 103 | if new_resource.property_is_set?(:encrypted_password) 104 | encrypted_password(pg_role_encrypted_password(new_resource.rolename)) 105 | end 106 | end 107 | 108 | action_class do 109 | include PostgreSQL::Cookbook::SqlHelpers::Role 110 | end 111 | 112 | action :create do 113 | return if pg_role?(new_resource.rolename) 114 | 115 | converge_if_changed( 116 | :superuser, 117 | :createdb, 118 | :createrole, 119 | :inherit, 120 | :login, 121 | :replication, 122 | :bypassrls, 123 | :connection_limit, 124 | :unencrypted_password, 125 | :encrypted_password, 126 | :valid_until, 127 | :in_role, 128 | :role, 129 | :admin 130 | ) do 131 | create_role(new_resource) 132 | end 133 | 134 | converge_if_changed(:config) { set_role_configuration(new_resource) } 135 | converge_if_changed(:unencrypted_password, :encrypted_password) { update_role_password(new_resource) } unless pg_role_password?(new_resource.rolename) 136 | end 137 | 138 | action :update do 139 | raise Chef::Exceptions::CurrentValueDoesNotExist, "Cannot update role '#{new_resource.rolename}' as it does not exist" unless pg_role?(new_resource.rolename) 140 | 141 | converge_if_changed(:superuser, :createdb, :createrole, :inherit, :login, :replication, :bypassrls, :connection_limit) do 142 | update_role(new_resource) 143 | end 144 | 145 | converge_if_changed(:config) { set_role_configuration(new_resource) } 146 | converge_if_changed(:unencrypted_password, :encrypted_password) { update_role_password(new_resource) } unless pg_role_password?(new_resource.rolename) 147 | end 148 | 149 | action :drop do 150 | converge_by("Drop role #{new_resource.rolename}") { drop_role(new_resource) } if pg_role?(new_resource.rolename) 151 | end 152 | 153 | action :delete do 154 | run_action(:drop) 155 | end 156 | 157 | action :set_password do 158 | converge_if_changed(:unencrypted_password, :encrypted_password) { update_role_password(new_resource) } 159 | end 160 | -------------------------------------------------------------------------------- /kitchen.yml: -------------------------------------------------------------------------------- 1 | --- 2 | driver: 3 | name: vagrant 4 | 5 | provisioner: 6 | name: chef_infra 7 | deprecations_as_errors: true 8 | product_name: chef 9 | chef_license: accept-no-persist 10 | product_version: <%= ENV['CHEF_VERSION'] || 'latest' %> 11 | log_level: <%= ENV['CHEF_LOG_LEVEL'] || 'auto' %> 12 | multiple_converge: 2 13 | enforce_idempotency: true 14 | 15 | verifier: 16 | name: inspec 17 | 18 | platforms: 19 | - name: almalinux-9 20 | - name: rockylinux-9 21 | - name: oraclelinux-9 22 | - name: centos-stream-9 23 | - name: amazonlinux-2023 24 | - name: debian-11 25 | - name: debian-12 26 | - name: ubuntu-22.04 27 | - name: ubuntu-24.04 28 | 29 | suites: 30 | - name: server_install_os 31 | verifier: 32 | inspec_tests: 33 | - path: test/integration/server_install_os/ 34 | run_list: 35 | - recipe[test::server_install_os] 36 | 37 | # PostgreSQL 17 38 | - name: access_17 39 | attributes: 40 | test: 41 | pg_ver: "17" 42 | verifier: 43 | inspec_tests: 44 | - path: test/integration/access/ 45 | run_list: 46 | - recipe[test::access] 47 | - name: client_install_17 48 | attributes: 49 | test: 50 | pg_ver: "17" 51 | verifier: 52 | inspec_tests: 53 | - path: test/integration/client_install/ 54 | inputs: 55 | pg_ver: "17" 56 | run_list: 57 | - recipe[test::client_install] 58 | - name: extension_17 59 | attributes: 60 | test: 61 | pg_ver: "17" 62 | verifier: 63 | inspec_tests: 64 | - path: test/integration/extension/ 65 | run_list: 66 | - recipe[test::extension] 67 | - name: ident_17 68 | attributes: 69 | test: 70 | pg_ver: "17" 71 | verifier: 72 | inspec_tests: 73 | - path: test/integration/ident/ 74 | run_list: 75 | - recipe[test::ident] 76 | - name: initdb_locale_17 77 | attributes: 78 | test: 79 | pg_ver: "17" 80 | verifier: 81 | inspec_tests: 82 | - path: test/integration/initdb_locale/ 83 | run_list: 84 | - recipe[test::initdb_locale] 85 | - name: repo_17 86 | attributes: 87 | test: 88 | pg_ver: "17" 89 | verifier: 90 | inspec_tests: 91 | - path: test/integration/repo/ 92 | inputs: 93 | pg_ver: "17" 94 | run_list: 95 | - recipe[test::repository] 96 | - name: server_install_17 97 | attributes: 98 | test: 99 | pg_ver: "17" 100 | verifier: 101 | inspec_tests: 102 | - path: test/integration/server_install/ 103 | inputs: 104 | pg_ver: "17" 105 | run_list: 106 | - recipe[test::server_install] 107 | 108 | # PostgreSQL 16 109 | - name: access_16 110 | attributes: 111 | test: 112 | pg_ver: "16" 113 | verifier: 114 | inspec_tests: 115 | - path: test/integration/access/ 116 | run_list: 117 | - recipe[test::access] 118 | - name: client_install_16 119 | attributes: 120 | test: 121 | pg_ver: "16" 122 | verifier: 123 | inspec_tests: 124 | - path: test/integration/client_install/ 125 | inputs: 126 | pg_ver: "16" 127 | run_list: 128 | - recipe[test::client_install] 129 | - name: extension_16 130 | attributes: 131 | test: 132 | pg_ver: "16" 133 | verifier: 134 | inspec_tests: 135 | - path: test/integration/extension/ 136 | run_list: 137 | - recipe[test::extension] 138 | - name: ident_16 139 | attributes: 140 | test: 141 | pg_ver: "16" 142 | verifier: 143 | inspec_tests: 144 | - path: test/integration/ident/ 145 | run_list: 146 | - recipe[test::ident] 147 | - name: initdb_locale_16 148 | attributes: 149 | test: 150 | pg_ver: "16" 151 | verifier: 152 | inspec_tests: 153 | - path: test/integration/initdb_locale/ 154 | run_list: 155 | - recipe[test::initdb_locale] 156 | - name: repo_16 157 | attributes: 158 | test: 159 | pg_ver: "16" 160 | verifier: 161 | inspec_tests: 162 | - path: test/integration/repo/ 163 | inputs: 164 | pg_ver: "16" 165 | run_list: 166 | - recipe[test::repository] 167 | - name: server_install_16 168 | attributes: 169 | test: 170 | pg_ver: "16" 171 | verifier: 172 | inspec_tests: 173 | - path: test/integration/server_install/ 174 | inputs: 175 | pg_ver: "16" 176 | run_list: 177 | - recipe[test::server_install] 178 | 179 | # PostgreSQL 15 180 | - name: access_15 181 | attributes: 182 | test: 183 | pg_ver: "15" 184 | verifier: 185 | inspec_tests: 186 | - path: test/integration/access/ 187 | run_list: 188 | - recipe[test::access] 189 | - name: client_install_15 190 | attributes: 191 | test: 192 | pg_ver: "15" 193 | verifier: 194 | inspec_tests: 195 | - path: test/integration/client_install/ 196 | inputs: 197 | pg_ver: "15" 198 | run_list: 199 | - recipe[test::client_install] 200 | - name: extension_15 201 | attributes: 202 | test: 203 | pg_ver: "15" 204 | verifier: 205 | inspec_tests: 206 | - path: test/integration/extension/ 207 | run_list: 208 | - recipe[test::extension] 209 | - name: ident_15 210 | attributes: 211 | test: 212 | pg_ver: "15" 213 | verifier: 214 | inspec_tests: 215 | - path: test/integration/ident/ 216 | run_list: 217 | - recipe[test::ident] 218 | - name: initdb_locale_15 219 | attributes: 220 | test: 221 | pg_ver: "15" 222 | verifier: 223 | inspec_tests: 224 | - path: test/integration/initdb_locale/ 225 | run_list: 226 | - recipe[test::initdb_locale] 227 | - name: repo_15 228 | attributes: 229 | test: 230 | pg_ver: "15" 231 | verifier: 232 | inspec_tests: 233 | - path: test/integration/repo/ 234 | inputs: 235 | pg_ver: "15" 236 | run_list: 237 | - recipe[test::repository] 238 | - name: server_install_15 239 | attributes: 240 | test: 241 | pg_ver: "15" 242 | verifier: 243 | inspec_tests: 244 | - path: test/integration/server_install/ 245 | inputs: 246 | pg_ver: "15" 247 | run_list: 248 | - recipe[test::server_install] 249 | - name: all_repos_install_15 250 | attributes: 251 | test: 252 | pg_ver: "15" 253 | verifier: 254 | inspec_tests: 255 | - path: test/integration/all_repos_install/ 256 | inputs: 257 | pg_ver: "15" 258 | run_list: 259 | - recipe[test::all_repos_install] 260 | - name: no_repos_install_15 261 | attributes: 262 | test: 263 | pg_ver: "15" 264 | verifier: 265 | inspec_tests: 266 | - path: test/integration/no_repos_install/ 267 | inputs: 268 | pg_ver: "15" 269 | run_list: 270 | - recipe[test::no_repos_install] 271 | -------------------------------------------------------------------------------- /libraries/ident.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Cookbook:: postgresql 3 | # Library:: ident 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | require_relative '_utils' 19 | 20 | module PostgreSQL 21 | module Cookbook 22 | module IdentHelpers 23 | module PgIdentTemplate 24 | def config_resource_init 25 | config_resource_create unless config_resource_exist? 26 | end 27 | 28 | def config_resource 29 | return unless config_resource_exist? 30 | 31 | find_resource!(:template, new_resource.config_file) 32 | end 33 | 34 | private 35 | 36 | def config_resource_exist? 37 | !find_resource!(:template, new_resource.config_file).nil? 38 | rescue Chef::Exceptions::ResourceNotFound 39 | false 40 | end 41 | 42 | def config_resource_create 43 | file = PostgreSQL::Cookbook::IdentHelpers::PgIdent::PgIdentFile.new 44 | file.read!(new_resource.config_file) if ::File.exist?(new_resource.config_file) 45 | 46 | with_run_context(:root) do 47 | declare_resource(:template, new_resource.config_file) do 48 | cookbook new_resource.cookbook 49 | source new_resource.source 50 | 51 | owner new_resource.owner 52 | group new_resource.group 53 | mode new_resource.filemode 54 | sensitive new_resource.sensitive 55 | 56 | variables(pg_ident: file) 57 | 58 | action :create 59 | delayed_action :create 60 | end 61 | end 62 | end 63 | end 64 | 65 | module PgIdent 66 | class PgIdentFile 67 | include PostgreSQL::Cookbook::Utils 68 | 69 | attr_reader :entries 70 | 71 | SPLIT_REGEX = /^(?[\w-]+)\s+(?[\w-]+)\s+(?[\w-]+)(?:\s*)(?#\s*.*)?$/ 72 | private_constant :SPLIT_REGEX 73 | 74 | def initialize 75 | @entries = [] 76 | @ident_entries = [] 77 | end 78 | 79 | def add(entry) 80 | raise unless entry.is_a?(PgIdentFileEntry) 81 | 82 | return false if entry?(entry.map_name) 83 | 84 | @entries.push(entry) 85 | 86 | sort! 87 | end 88 | 89 | def entry(map_name) 90 | entry = @entries.filter { |e| e.map_name.eql?(map_name) } 91 | 92 | return if nil_or_empty?(entry) 93 | 94 | raise PgIdentFileDuplicateEntry, "Duplicate entries found for #{map_name}" unless entry.one? 95 | 96 | entry.pop 97 | end 98 | 99 | def entry?(map_name) 100 | !@entries.none? { |e| e.map_name.eql?(map_name) } 101 | end 102 | 103 | def include?(entry) 104 | raise unless entry.is_a?(PgIdentFileEntry) 105 | 106 | @entries.any? { |e| e.map_name.eql?(entry.map_name) } 107 | end 108 | 109 | def read!(file = 'pg_ident.conf', sort: true) 110 | raise ArgumentError, "File #{file} does not exist" unless ::File.exist?(file) 111 | 112 | @ident_entries.concat(File.read(file).split("\n")) 113 | @ident_entries.reject! { |l| l.start_with?('#') } 114 | @ident_entries.reject!(&:empty?) 115 | @ident_entries.map! { |l| l.gsub(/\s+/, ' ') } 116 | 117 | split_entries 118 | 119 | marshall_entries 120 | sort! if sort 121 | end 122 | 123 | def remove(entry) 124 | raise unless entry.is_a?(PgIdentFileEntry) || entry.is_a?(String) 125 | 126 | remove_name = case entry 127 | when PgIdentFileEntry 128 | entry.map_name 129 | when String 130 | entry 131 | end 132 | 133 | @entries.reject! { |e| e.map_name.eql?(remove_name) } 134 | end 135 | 136 | def sort! 137 | @entries.sort_by!(&:map_name) 138 | end 139 | 140 | def to_s(sort: true) 141 | sort! if sort 142 | @entries.map(&:to_s).join("\n") 143 | end 144 | 145 | def self.read(file = 'pg_ident.conf', sort: true) 146 | pg_hba = new 147 | pg_hba.read!(file, sort:) 148 | 149 | pg_hba 150 | end 151 | 152 | private 153 | 154 | def split_entries 155 | return if @ident_entries.empty? 156 | 157 | @ident_entries.map! { |entry| SPLIT_REGEX.match(entry).named_captures.compact.transform_keys(&:to_sym) } 158 | end 159 | 160 | def marshall_entries 161 | return if @ident_entries.empty? 162 | 163 | @ident_entries.each { |entry| @entries.push(PgIdentFileEntry.new(**entry)) } 164 | 165 | @entries 166 | end 167 | end 168 | 169 | class PgIdentFileEntry 170 | include PostgreSQL::Cookbook::Utils 171 | 172 | attr_reader :map_name, :system_username, :database_username, :comment 173 | 174 | ENTRY_FIELD_FORMAT = { 175 | map_name: 16, 176 | system_username: 24, 177 | database_username: 24, 178 | comment: 0, 179 | }.freeze 180 | ENTRY_FIELDS = ENTRY_FIELD_FORMAT.keys.dup.freeze 181 | 182 | private_constant :ENTRY_FIELD_FORMAT, :ENTRY_FIELDS 183 | 184 | def initialize(map_name:, system_username:, database_username:, comment: nil) 185 | @map_name = map_name 186 | @system_username = system_username 187 | @database_username = database_username 188 | @comment = comment 189 | end 190 | 191 | def to_s 192 | entry_string = '' 193 | ENTRY_FIELD_FORMAT.each do |field, ljust_count| 194 | field = respond_to?(field) ? send(field) : '' 195 | field_string = field.to_s.ljust(ljust_count) 196 | entry_string.concat(field_string) 197 | end 198 | 199 | entry_string.strip 200 | end 201 | 202 | def eql?(other) 203 | return false unless self.class.eql?(other.class) 204 | 205 | return true if self.class.const_get(:ENTRY_FIELDS).all? { |field| send(field).eql?(other.send(field)) } 206 | 207 | false 208 | end 209 | 210 | def update(system_username:, database_username:, comment:) 211 | @system_username = system_username if system_username 212 | @database_username = database_username if database_username 213 | self.comment = comment if comment 214 | 215 | self 216 | end 217 | 218 | def comment=(comment) 219 | @comment = comment 220 | @comment = "# #{@comment}" unless nil_or_empty?(@comment) || @comment.start_with?('#') 221 | end 222 | end 223 | 224 | class PgIdentFileDuplicateEntry < StandardError; end 225 | end 226 | end 227 | end 228 | end 229 | -------------------------------------------------------------------------------- /libraries/sql/_connection.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Cookbook:: postgresql 3 | # Library:: sql/_connection 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | require_relative '../_utils' 19 | require_relative '../helpers' 20 | 21 | module PostgreSQL 22 | module Cookbook 23 | module SqlHelpers 24 | module Connection 25 | private 26 | 27 | include PostgreSQL::Cookbook::Utils 28 | include PostgreSQL::Cookbook::Helpers 29 | 30 | def postgresql_devel_pkg_name(version: installed_postgresql_major_version, source: installed_postgresql_package_source) 31 | case node['platform_family'] 32 | when 'rhel' 33 | source.eql?(:repo) ? "postgresql#{version}-devel" : 'postgresql-devel' 34 | when 'debian' 35 | 'libpq-dev' 36 | when 'amazon' 37 | 'libpq-devel' 38 | end 39 | end 40 | 41 | def postgresql_devel_path(suffix = nil, version: installed_postgresql_major_version) 42 | path = case node['platform_family'] 43 | when 'rhel', 'amazon' 44 | "/usr/pgsql-#{version}" 45 | when 'debian' 46 | '/usr/include/postgresql' 47 | else 48 | raise "Unsupported platform family #{node['platform_family']}" 49 | end 50 | 51 | path = ::File.join(path, suffix) unless nil_or_empty?(suffix) 52 | 53 | path 54 | end 55 | 56 | def pg_gem_build_options 57 | case node['platform_family'] 58 | when 'rhel', 'amazon' 59 | "--platform ruby -- --with-pg-include=#{postgresql_devel_path('include')} --with-pg-lib=#{postgresql_devel_path('lib')}" 60 | when 'debian' 61 | "--platform ruby -- --with-pg-include=#{postgresql_devel_path} --with-pg-lib=#{postgresql_devel_path}" 62 | else 63 | raise "Unsupported platform family #{node['platform_family']}" 64 | end 65 | end 66 | 67 | def install_pg_gem 68 | return if gem_installed?('pg') 69 | 70 | libpq_package_name = case installed_postgresql_package_source 71 | when :os 72 | 'libpq' 73 | when :repo 74 | 'libpq5' 75 | end 76 | 77 | if platform_family?('rhel') 78 | case node['platform_version'].to_i 79 | when 7 80 | declare_resource(:package, 'epel-release') { compile_time(true) } 81 | declare_resource(:package, 'centos-release-scl') { compile_time(true) } 82 | when 8 83 | declare_resource(:package, libpq_package_name) { compile_time(true) } 84 | declare_resource(:package, 'perl-IPC-Run') do 85 | compile_time(true) 86 | case node['platform'] 87 | when 'oracle' 88 | options ['--enablerepo=ol8_codeready_builder'] 89 | when 'redhat' 90 | options('--enablerepo=codeready-builder-for-rhel-8') 91 | else 92 | # CentOS Stream, AlmaLinux, Rocky Linux use powertools 93 | options('--enablerepo=powertools') 94 | end 95 | end 96 | when 9 97 | declare_resource(:package, libpq_package_name) { compile_time(true) } 98 | declare_resource(:package, 'perl-IPC-Run') do 99 | compile_time(true) 100 | if platform?('oracle') 101 | options ['--enablerepo=ol9_codeready_builder'] 102 | else 103 | options('--enablerepo=crb') 104 | end 105 | end 106 | when 10 107 | declare_resource(:package, libpq_package_name) { compile_time(true) } 108 | declare_resource(:package, 'perl-IPC-Run') do 109 | compile_time(true) 110 | if platform?('oracle') 111 | options ['--enablerepo=ol10_codeready_builder'] 112 | else 113 | options('--enablerepo=crb') 114 | end 115 | end 116 | end 117 | end 118 | 119 | declare_resource(:build_essential, 'Build Essential') { compile_time(true) } 120 | declare_resource(:package, postgresql_devel_pkg_name) { compile_time(true) } 121 | 122 | build_options = pg_gem_build_options 123 | declare_resource(:chef_gem, 'pg') do 124 | options build_options 125 | version '~> 1.4' 126 | compile_time true 127 | end 128 | end 129 | 130 | def pg_connection_params 131 | scope = respond_to?(:new_resource) ? new_resource : self 132 | 133 | return scope.connection_string unless nil_or_empty?(scope.connection_string) 134 | 135 | %i(host port options dbname user password).map do |p| 136 | next unless scope.respond_to?(p) 137 | 138 | [ p, scope.send(p) ] 139 | end.to_h.compact 140 | end 141 | 142 | def pg_client 143 | install_pg_gem unless gem_installed?('pg') 144 | 145 | raise 'pg Gem Missing' unless gem_installed?('pg') 146 | 147 | require 'pg' unless defined?(::PG) 148 | 149 | connection_params = pg_connection_params 150 | Chef::Log.debug("Got params: [#{connection_params.class}] #{connection_params}") 151 | 152 | host = connection_params.fetch(:host, nil) || :local_socket 153 | dbname = connection_params.fetch(:dbname, 'postgres') 154 | # Use the actual port that will be used (PG defaults to 5432) 155 | port = connection_params.fetch(:port, 5432) 156 | client = node.run_state.dig('postgresql_pg_connection', host, port, dbname) 157 | 158 | if client.is_a?(::PG::Connection) 159 | Chef::Log.info("Returning pre-existing client for #{host}:#{port}/#{dbname}") 160 | return client 161 | end 162 | 163 | Chef::Log.info("Creating client for #{host}:#{port}/#{dbname}") 164 | 165 | # The Chef Infra Client runs as user `root`. In local connnection 166 | # mode we have to connect as local user `postgres` to the socket. 167 | # This is needed to pass peer authentication for the database 168 | # superuser. 169 | if host == :local_socket 170 | Process::UID.eid = Process::UID.from_name('postgres') 171 | end 172 | 173 | begin 174 | client = ::PG::Connection.new(**connection_params) 175 | ensure 176 | if host == :local_socket 177 | # Switch back to elevated privileges 178 | Process::UID.switch 179 | end 180 | end 181 | 182 | client.type_map_for_queries = PG::BasicTypeMapForQueries.new(client) 183 | 184 | node.run_state['postgresql_pg_connection'] ||= {} 185 | node.run_state['postgresql_pg_connection'][host] ||= {} 186 | node.run_state['postgresql_pg_connection'][host][port] ||= {} 187 | node.run_state['postgresql_pg_connection'][host][port][dbname] = client 188 | 189 | node.run_state['postgresql_pg_connection'][host][port][dbname] 190 | end 191 | 192 | def execute_sql(query, max_one_result: false) 193 | Chef::Log.debug("Executing query: #{query}") 194 | result = pg_client.exec(query).to_a 195 | 196 | Chef::Log.debug("Got result: #{result}") 197 | return if result.empty? 198 | 199 | raise "Expected a single result, got #{result.count}" unless result.one? || !max_one_result 200 | 201 | result 202 | end 203 | 204 | def execute_sql_params(query, params, max_one_result: false) 205 | Chef::Log.debug("Executing query: #{query} with params: #{params}") 206 | result = pg_client.exec_params(query, params).to_a 207 | 208 | Chef::Log.debug("Got result: #{result}") 209 | return if result.empty? 210 | 211 | raise "Expected a single result, got #{result.count}" unless result.one? || !max_one_result 212 | 213 | result 214 | end 215 | end 216 | end 217 | end 218 | end 219 | -------------------------------------------------------------------------------- /libraries/helpers.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Cookbook:: postgresql 3 | # Library:: helpers 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | require_relative '_utils' 19 | require 'securerandom' 20 | 21 | module PostgreSQL 22 | module Cookbook 23 | module Helpers 24 | include Utils 25 | 26 | def installed_postgresql_package_logic 27 | pgsql_package = node['packages'].filter { |p| p.match?(/^postgresql-?(\d+)?$/) }.values 28 | 29 | unless pgsql_package.one? 30 | versions = pgsql_package.map { |values| values['version'] }.join(', ') 31 | Chef::Log.warn("Detected #{pgsql_package.count} installed PostgreSQL versions: #{versions}. Using latest version.") 32 | pgsql_package = [pgsql_package.max_by { |values| values['version'].to_f }] 33 | end 34 | 35 | raise 'Unable to determine installed PostgreSQL version' if nil_or_empty?(pgsql_package) 36 | 37 | pgsql_package.first 38 | end 39 | 40 | def installed_postgresql_major_version 41 | pgsql_package = installed_postgresql_package_logic 42 | pgsql_package_version = pgsql_package.fetch('version').to_i 43 | pgsql_package_source = if pgsql_package.key?('release') 44 | pgsql_package.fetch('release').match?('PGDG') ? :repo : :os 45 | else 46 | pgsql_package.fetch('version').match?('pgdg') ? :repo : :os 47 | end 48 | 49 | Chef::Log.info("Detected installed PostgreSQL version: #{pgsql_package_version} installed from #{pgsql_package_source}") 50 | 51 | pgsql_package_version 52 | end 53 | 54 | def installed_postgresql_package_source 55 | pgsql_package = installed_postgresql_package_logic 56 | pgsql_package_version = pgsql_package.fetch('version').to_i 57 | pgsql_package_source = if pgsql_package.key?('release') 58 | pgsql_package.fetch('release').match?('PGDG') ? :repo : :os 59 | else 60 | pgsql_package.fetch('version').match?('pgdg') ? :repo : :os 61 | end 62 | 63 | Chef::Log.info("Detected installed PostgreSQL version: #{pgsql_package_version} installed from #{pgsql_package_source}") 64 | 65 | pgsql_package_source 66 | end 67 | 68 | def data_dir(version: installed_postgresql_major_version, source: installed_postgresql_package_source) 69 | case node['platform_family'] 70 | when 'rhel', 'amazon' 71 | source.eql?(:repo) ? "/var/lib/pgsql/#{version}/data" : '/var/lib/pgsql/data' 72 | when 'debian' 73 | "/var/lib/postgresql/#{version}/main" 74 | end 75 | end 76 | 77 | def conf_dir(version: installed_postgresql_major_version, source: installed_postgresql_package_source) 78 | case node['platform_family'] 79 | when 'rhel', 'amazon' 80 | source.eql?(:repo) ? "/var/lib/pgsql/#{version}/data" : '/var/lib/pgsql/data' 81 | when 'debian' 82 | "/etc/postgresql/#{version}/main" 83 | end 84 | end 85 | 86 | # determine the platform specific service name 87 | def default_platform_service_name(version: installed_postgresql_major_version, source: installed_postgresql_package_source) 88 | if platform_family?('rhel', 'amazon') && source.eql?(:repo) 89 | "postgresql-#{version}" 90 | else 91 | 'postgresql' 92 | end 93 | end 94 | 95 | def follower? 96 | ::File.exist? "#{data_dir}/recovery.conf" 97 | end 98 | 99 | def initialized? 100 | return true if ::File.exist?("#{conf_dir}/PG_VERSION") 101 | false 102 | end 103 | 104 | def secure_random 105 | r = SecureRandom.hex 106 | Chef::Log.debug "Generated password: #{r}" 107 | 108 | r 109 | end 110 | 111 | def default_server_packages(version: nil, source: :os) 112 | case node['platform_family'] 113 | when 'rhel' 114 | { 115 | os: %w(libpq postgresql-contrib postgresql-server), 116 | repo: %W(postgresql#{version.delete('.')}-contrib postgresql#{version.delete('.')}-server), 117 | }.fetch(source, nil) 118 | when 'amazon' 119 | { 120 | os: %W(postgresql#{version.delete('.')}-contrib postgresql#{version.delete('.')}-server), 121 | repo: %W(postgresql#{version.delete('.')}-contrib postgresql#{version.delete('.')}-server), 122 | }.fetch(source, nil) 123 | when 'debian' 124 | { 125 | os: %w(libpq5 postgresql postgresql-common), 126 | repo: %W(postgresql-#{version} postgresql-common), 127 | }.fetch(source, nil) 128 | end 129 | end 130 | 131 | def default_client_packages(version: nil, source: :os) 132 | case node['platform_family'] 133 | when 'rhel' 134 | { 135 | os: %w(postgresql), 136 | repo: %W(postgresql#{version.delete('.')}), 137 | }.fetch(source, nil) 138 | when 'amazon' 139 | { 140 | os: %W(postgresql#{version.delete('.')}), 141 | repo: %W(postgresql#{version.delete('.')}), 142 | }.fetch(source, nil) 143 | when 'debian' 144 | { 145 | os: %w(postgresql-client), 146 | repo: %W(postgresql-client-#{version}), 147 | }.fetch(source, nil) 148 | end 149 | end 150 | 151 | def default_yum_gpg_key_uri 152 | if platform_family?('rhel') 153 | rhel_version = node['platform_version'].to_i 154 | arch = node['kernel']['machine'] 155 | 156 | if rhel_version == 7 157 | arch == 'aarch64' ? 'https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-AARCH64-RHEL7' : 'https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-RHEL7' 158 | elsif arch == 'aarch64' 159 | 'https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-AARCH64-RHEL' 160 | else 161 | 'https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-RHEL' 162 | end 163 | else 164 | 'https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-RHEL' 165 | end 166 | end 167 | 168 | def dnf_module_platform? 169 | platform_family?('rhel') && node['platform_version'].to_i.between?(8, 9) 170 | end 171 | 172 | # determine the appropriate DB init command to run based on RHEL/Amazon release 173 | # initdb defaults to the execution environment. 174 | # https://www.postgresql.org/docs/9.5/static/locale.html 175 | def rhel_init_db_command(new_resource) 176 | cmd = new_resource.source.eql?(:repo) ? "/usr/pgsql-#{new_resource.version}/bin/initdb" : '/usr/bin/initdb' 177 | cmd << " --locale '#{new_resource.initdb_locale}'" if new_resource.initdb_locale 178 | cmd << " -E '#{new_resource.initdb_encoding}'" if new_resource.initdb_encoding 179 | cmd << " #{new_resource.initdb_additional_options}" if new_resource.initdb_additional_options 180 | cmd << " -D '#{data_dir}'" 181 | end 182 | 183 | # Given the base URL build the complete URL string for a yum repo 184 | def yum_repo_url(base_url) 185 | "#{base_url}/#{new_resource.version}/redhat/#{yum_repo_platform_string}" 186 | end 187 | 188 | # Given the base URL build the complete URL string for a yum repo 189 | def yum_common_repo_url 190 | "https://download.postgresql.org/pub/repos/yum/common/redhat/#{yum_repo_platform_string}" 191 | end 192 | 193 | # Build the platform string that makes up the final component of the yum repo URL 194 | def yum_repo_platform_string 195 | release = platform?('amazon') ? '8' : '$releasever' 196 | "rhel-#{release}-$basearch" 197 | end 198 | 199 | # On Amazon use the RHEL 8 packages. Otherwise use the releasever yum variable 200 | def yum_releasever 201 | platform?('amazon') ? '8' : '$releasever' 202 | end 203 | 204 | # Generate a password if the value is set to generate. 205 | def postgres_password(new_resource) 206 | new_resource.password == 'generate' ? secure_random : new_resource.password 207 | end 208 | end 209 | end 210 | end 211 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------