Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better handling of extension versions #163

Merged
merged 21 commits into from
Nov 24, 2021
Merged
Changes from 4 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
dea5cbc
Initial changes to postgresql extension handling
keithf4 Oct 29, 2021
db473ab
Properly handle latest version flag. Formatting fixes.
keithf4 Nov 1, 2021
2aa7797
Merge remote-tracking branch 'upstream/main' into extension_versioning
keithf4 Nov 15, 2021
0c4f4be
Fix variable usage. Also make param usage consistent. Update docs.
keithf4 Nov 15, 2021
2ec1b96
Update plugins/modules/postgresql_ext.py
keithf4 Nov 16, 2021
709febe
Update plugins/modules/postgresql_ext.py
keithf4 Nov 16, 2021
7bbaaf2
Fix CI test failures
keithf4 Nov 16, 2021
8ae6853
Wrap long doc line
keithf4 Nov 16, 2021
360ec49
Fix invalid yaml in examples
keithf4 Nov 16, 2021
e4748cb
Fix handling given version on extension creation. Use safe sql string…
keithf4 Nov 16, 2021
2fae4ea
Fix CI test failures
keithf4 Nov 16, 2021
f85df46
Revert safe sql string building. Not supported in CentOS7/U20
keithf4 Nov 17, 2021
206f1fc
Fix CI Test on Ubuntu to use new extension update syntax for latest v…
keithf4 Nov 17, 2021
81cdbdb
Fix CI test for U20 when updating to latest version
keithf4 Nov 17, 2021
5fc4758
Fix CI test for non-existent extension. Update python doc.
keithf4 Nov 17, 2021
14f6c3b
Remove unused codepath
keithf4 Nov 19, 2021
d669797
Remove extra comment marker
keithf4 Nov 19, 2021
427e767
Add changlog fragment
keithf4 Nov 19, 2021
5a2d993
Apply suggestions from code review
keithf4 Nov 22, 2021
317a8fd
Make available_extensions a set. Remove SELECT from executed queries.…
keithf4 Nov 23, 2021
883c01d
Merge branch 'extension_versioning' of github.com:keithf4/community.p…
keithf4 Nov 23, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 77 additions & 82 deletions plugins/modules/postgresql_ext.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,9 @@
description:
- Extension version to add or update to. Has effect with I(state=present) only.
- If not specified, the latest extension version will be created.
- It can't downgrade an extension version.
When version downgrade is needed, remove the extension and create new one with appropriate version.
- Set I(version=latest) to update the extension to the latest available version.
- Version downgrade is only supported if extension provides a downgrade path.
Otherwise extension must be removed and lower version of extension must be made available.
keithf4 marked this conversation as resolved.
Show resolved Hide resolved
- Set I(version=latest) to always update the extension to the latest available version.
type: str
trust_input:
description:
Expand Down Expand Up @@ -114,6 +114,7 @@
- Thomas O'Donnell (@andytom)
- Sandro Santilli (@strk)
- Andrew Klychkov (@Andersson007)
- Keith Fiske (@keithf4)
extends_documentation_fragment:
- community.postgresql.postgres

Expand Down Expand Up @@ -147,13 +148,13 @@
cascade: yes
state: absent

- name: Create extension foo of version 1.2 or update it if it's already created
- name: Create extension foo of version 1.2 or update it to that version if it's already created and a valid update path exists
community.postgresql.postgresql_ext:
db: acme
name: foo
version: 1.2

- name: Assuming extension foo is created, update it to the latest version
- name: Create latest available version of extension foo. If already installed, update it to the latest version
keithf4 marked this conversation as resolved.
Show resolved Hide resolved
community.postgresql.postgresql_ext:
db: acme
name: foo
Expand All @@ -171,8 +172,6 @@

import traceback

from distutils.version import LooseVersion

try:
from psycopg2.extras import DictCursor
except ImportError:
Expand All @@ -198,14 +197,9 @@
# PostgreSQL module specific support methods.
#

def ext_exists(cursor, ext):
query = "SELECT * FROM pg_extension WHERE extname=%(ext)s"
cursor.execute(query, {'ext': ext})
return cursor.rowcount == 1


def ext_delete(cursor, ext, cascade):
if ext_exists(cursor, ext):
def ext_delete(cursor, ext, curr_version, cascade):
if curr_version:
query = "DROP EXTENSION \"%s\"" % ext
if cascade:
query += " CASCADE"
Expand Down Expand Up @@ -245,7 +239,7 @@ def ext_create(cursor, ext, schema, cascade, version):

if schema:
query += " WITH SCHEMA \"%s\"" % schema
if version:
if version != 'latest':
query += " VERSION %(ver)s"
params['ver'] = version
if cascade:
Expand All @@ -272,60 +266,59 @@ def ext_get_versions(cursor, ext):
ext (str) -- extension name
"""

current_version = None
params = {}
params['ext'] = ext

# 1. Get the current extension version:
query = ("SELECT extversion FROM pg_catalog.pg_extension "
"WHERE extname = %(ext)s")

current_version = None
cursor.execute(query, {'ext': ext})
cursor.execute(query, params)

res = cursor.fetchone()
if res:
current_version = res[0]

# 2. Get available versions:
query = ("SELECT version FROM pg_available_extension_versions "
"WHERE name = %(ext)s")
cursor.execute(query, {'ext': ext})
res = cursor.fetchall()

available_versions = parse_ext_versions(current_version, res)
cursor.execute(query, params)

available_versions = [r[0] for r in cursor.fetchall()]
keithf4 marked this conversation as resolved.
Show resolved Hide resolved

if current_version is None:
current_version = False

return (current_version, available_versions)


def parse_ext_versions(current_version, ext_ver_list):
"""Parse ext versions.

Args:
current_version (str) -- version to compare elements of ext_ver_list with
ext_ver_list (list) -- list containing dicts with versions

Return a sorted list with versions that are higher than current_version.

Note: Incomparable versions (e.g., postgis version "unpackaged") are skipped.
def ext_valid_update_path(cursor, ext, current_version, version):
"""
Value of 'latest' is always a valid path.
"""
available_versions = []

for line in ext_ver_list:
if line['version'] == 'unpackaged':
continue
valid_path = False
params = {}
if version != 'latest':
query = ("SELECT path FROM pg_extension_update_paths(%(ext)s)"
"WHERE source = %(cv)s"
"AND target = %(ver)s")

try:
if current_version is None:
if LooseVersion(line['version']) >= LooseVersion('0'):
available_versions.append(line['version'])
else:
if LooseVersion(line['version']) > LooseVersion(current_version):
available_versions.append(line['version'])
except Exception:
# When a version cannot be compared, skip it
# (there's a note in the documentation)
continue
params['ext'] = ext
params['cv'] = current_version
params['ver'] = version

cursor.execute(query, params)
res = cursor.fetchone()
if res != None:
valid_path = True
else:
valid_path = True

return (valid_path)

return sorted(available_versions, key=LooseVersion)

# ===========================================
# Module execution.
Expand Down Expand Up @@ -374,65 +367,67 @@ def main():
curr_version, available_versions = ext_get_versions(cursor, ext)

if state == "present":
if version == 'latest':
if available_versions:
version = available_versions[-1]
else:
version = ''

# If version passed
if version:
# If the specific version is passed and it is not available for update:
if version not in available_versions:
if not curr_version:
module.fail_json(msg="Passed version '%s' is not available" % version)

elif LooseVersion(curr_version) == LooseVersion(version):
# If extension is installed, update to passed version if a valid path exists
if curr_version:
# Given version already installed
if curr_version == version:
changed = False

else:
module.fail_json(msg="Passed version '%s' is lower than "
"the current created version '%s' or "
"the passed version is not available" % (version, curr_version))

# If the specific version is passed and it is higher that the current version:
if curr_version:
if LooseVersion(curr_version) < LooseVersion(version):
valid_update_path = ext_valid_update_path(cursor, ext, curr_version, version)
if valid_update_path:
if module.check_mode:
changed = True
else:
changed = ext_update_version(cursor, ext, version)
else:
module.fail_json(msg="Passed version '%s' has no valid update path from "
"the currently installed version '%s' or "
"the passed version is not available" % (version, curr_version))
else:
# If not requesting latest version and passed version not available
if version != 'latest' and version not in available_versions:
module.fail_json(msg="Passed version '%s' is not available" % version)
# Else install the passed version when available
else:
if module.check_mode:
changed = True
else:
changed = ext_update_version(cursor, ext, version)

# If the specific version is passed and it is created now:
if curr_version == version:
changed = False
changed = ext_create(cursor, ext, schema, cascade, 'latest')

# If the ext doesn't exist and installed:
elif not curr_version and available_versions:
# If version is not passed:
else:
# Extension exists, attempt to update to latest version defined in extension control file
# ALTER EXTENSION is actually run, so 'changed' is technically true even if nothing updated
if curr_version and version == 'latest':
if module.check_mode:
changed = True
else:
changed = ext_create(cursor, ext, schema, cascade, version)

# If version is not passed:
else:
if not curr_version:
# If the ext doesn't exist and it's installed:
changed = ext_update_version(cursor, ext, version)
# Extension exists, no request to update so no change
elif curr_version:
changed = False
else:
# If the ext doesn't exist and is available:
if available_versions:
if module.check_mode:
changed = True
else:
changed = ext_create(cursor, ext, schema, cascade, version)
changed = ext_create(cursor, ext, schema, cascade, 'latest')

# If the ext doesn't exist and not installed:
# If the ext doesn't exist and is not available:
else:
module.fail_json(msg="Extension %s is not installed" % ext)
module.fail_json(msg="Extension %s is not available" % ext)


elif state == "absent":
if curr_version:
if module.check_mode:
changed = True
else:
changed = ext_delete(cursor, ext, cascade)
changed = ext_delete(cursor, ext, curr_version, cascade)
else:
changed = False

Expand Down