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

[DPE-3451][DPE-3452] Expose pgbouncer #137

Merged
merged 29 commits into from
Feb 16, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
12b821f
Update libs
dragomirp Feb 1, 2024
e03b938
Split off data-integrator test
dragomirp Feb 1, 2024
da4c404
Hotpatching test app
dragomirp Feb 1, 2024
bfb80b2
TLS
dragomirp Feb 1, 2024
94197f1
Relation checks
dragomirp Feb 2, 2024
af628bf
Remove tls test
dragomirp Feb 2, 2024
2f6eb18
Remove TLS
dragomirp Feb 2, 2024
0c78195
Use fetch data
dragomirp Feb 2, 2024
86299b0
Merge branch 'main' into dpe-3451-expose
dragomirp Feb 2, 2024
f852e33
Switch to hotpatching data-integrator
dragomirp Feb 2, 2024
bc31dd7
Don't assume a single snap and single set of certs
dragomirp Feb 2, 2024
cbb6b81
Merge branch 'main' into dpe-3451-expose
dragomirp Feb 4, 2024
dc5660a
Merge branch 'main' into dpe-3451-expose
dragomirp Feb 5, 2024
344705c
Merge branch 'main' into dpe-3451-expose
dragomirp Feb 5, 2024
8b945e4
Open port
dragomirp Feb 5, 2024
19297a1
Merge branch 'main' into dpe-3451-expose
dragomirp Feb 6, 2024
696fc38
Increase timeouts
dragomirp Feb 6, 2024
0b5ea8a
Wrong push TLS files method signature
dragomirp Feb 6, 2024
48c983b
Fix secret keys
dragomirp Feb 6, 2024
21464eb
Update secrets loop
dragomirp Feb 7, 2024
e40574a
Rename flag
dragomirp Feb 7, 2024
babf67d
Bump libs
dragomirp Feb 7, 2024
23e7b9e
Merge remote-tracking branch 'origin/fix-secrets' into dpe-3451-expose
dragomirp Feb 7, 2024
02fbff7
Merge branch 'main' into dpe-3451-expose
dragomirp Feb 8, 2024
ea13194
Merge branch 'main' into dpe-3451-expose
dragomirp Feb 10, 2024
96e3a26
Merge branch 'main' into dpe-3451-expose
dragomirp Feb 12, 2024
e94998c
Remove hotpatching
dragomirp Feb 15, 2024
53542f3
Merge branch 'main' into dpe-3451-expose
dragomirp Feb 15, 2024
3055c3b
Merge branch 'main' into dpe-3451-expose
dragomirp Feb 15, 2024
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
8 changes: 7 additions & 1 deletion actions.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# Copyright 2023 Canonical Ltd.
# Copyright 2024 Canonical Ltd.
# See LICENSE file for licensing details.

pre-upgrade-check:
description: Run necessary pre-upgrade checks before executing a charm upgrade.

set-tls-private-key:
description: Set the private key, which will be used for certificate signing requests (CSR). Run for each unit separately.
params:
private-key:
type: string
description: The content of private key for communications with clients. Content will be auto-generated if this option is not specified.
6 changes: 6 additions & 0 deletions charmcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ parts:
echo 'ERROR: Use "tox run -e build" instead of calling "charmcraft pack" directly' >&2
exit 1
fi
build-packages:
- libffi-dev
- libssl-dev
- rustc
- cargo
- pkg-config
charm-strict-dependencies: true
libpq:
build-packages:
Expand Down
158 changes: 92 additions & 66 deletions lib/charms/data_platform_libs/v0/data_interfaces.py

Large diffs are not rendered by default.

22 changes: 20 additions & 2 deletions lib/charms/postgresql_k8s/v0/postgresql.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
Any charm using this library should import the `psycopg2` or `psycopg2-binary` dependency.
"""
import logging
from collections import OrderedDict
from typing import Dict, List, Optional, Set, Tuple

import psycopg2
Expand All @@ -34,10 +35,21 @@

# Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version
LIBPATCH = 21
LIBPATCH = 22

INVALID_EXTRA_USER_ROLE_BLOCKING_MESSAGE = "invalid role(s) for extra user roles"

REQUIRED_PLUGINS = {
"address_standardizer": ["postgis"],
"address_standardizer_data_us": ["postgis"],
"jsonb_plperl": ["plperl"],
"postgis_raster": ["postgis"],
"postgis_tiger_geocoder": ["postgis", "fuzzystrmatch"],
"postgis_topology": ["postgis"],
}
DEPENDENCY_PLUGINS = set()
for dependencies in REQUIRED_PLUGINS.values():
DEPENDENCY_PLUGINS |= set(dependencies)

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -289,12 +301,18 @@ def enable_disable_extensions(self, extensions: Dict[str, bool], database: str =
cursor.execute("SELECT datname FROM pg_database WHERE NOT datistemplate;")
databases = {database[0] for database in cursor.fetchall()}

ordered_extensions = OrderedDict()
for plugin in DEPENDENCY_PLUGINS:
ordered_extensions[plugin] = extensions.get(plugin, False)
for extension, enable in extensions.items():
ordered_extensions[extension] = enable

# Enable/disabled the extension in each database.
for database in databases:
with self._connect_to_database(
database=database
) as connection, connection.cursor() as cursor:
for extension, enable in extensions.items():
for extension, enable in ordered_extensions.items():
cursor.execute(
f"CREATE EXTENSION IF NOT EXISTS {extension};"
if enable
Expand Down
220 changes: 220 additions & 0 deletions lib/charms/postgresql_k8s/v0/postgresql_tls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
# Copyright 2022 Canonical Ltd.
# See LICENSE file for licensing details.

"""In this class we manage certificates relation.

This class handles certificate request and renewal through
the interaction with the TLS Certificates Operator.

This library needs that https://charmhub.io/tls-certificates-interface/libraries/tls_certificates
library is imported to work.

It also needs the following methods in the charm class:
— get_hostname_by_unit: to retrieve the DNS hostname of the unit.
— get_secret: to retrieve TLS files from secrets.
— push_tls_files_to_workload: to push TLS files to the workload container and enable TLS.
— set_secret: to store TLS files as secrets.
— update_config: to disable TLS when relation with the TLS Certificates Operator is broken.
"""

import base64
import ipaddress
import logging
import re
import socket
from typing import List, Optional

from charms.tls_certificates_interface.v2.tls_certificates import (
CertificateAvailableEvent,
CertificateExpiringEvent,
TLSCertificatesRequiresV2,
generate_csr,
generate_private_key,
)
from ops.charm import ActionEvent, RelationBrokenEvent
from ops.framework import Object
from ops.pebble import ConnectionError, PathError, ProtocolError

# The unique Charmhub library identifier, never change it
LIBID = "c27af44a92df4ef38d7ae06418b2800f"

# Increment this major API version when introducing breaking changes
LIBAPI = 0

# Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version.
LIBPATCH = 8

logger = logging.getLogger(__name__)
SCOPE = "unit"
TLS_RELATION = "certificates"


class PostgreSQLTLS(Object):
"""In this class we manage certificates relation."""

def __init__(
self, charm, peer_relation: str, additional_dns_names: Optional[List[str]] = None
):
"""Manager of PostgreSQL relation with TLS Certificates Operator."""
super().__init__(charm, "client-relations")
self.charm = charm
self.peer_relation = peer_relation
self.additional_dns_names = additional_dns_names or []
self.certs = TLSCertificatesRequiresV2(self.charm, TLS_RELATION)
self.framework.observe(
self.charm.on.set_tls_private_key_action, self._on_set_tls_private_key
)
self.framework.observe(
self.charm.on[TLS_RELATION].relation_joined, self._on_tls_relation_joined
)
self.framework.observe(
self.charm.on[TLS_RELATION].relation_broken, self._on_tls_relation_broken
)
self.framework.observe(self.certs.on.certificate_available, self._on_certificate_available)
self.framework.observe(self.certs.on.certificate_expiring, self._on_certificate_expiring)

def _on_set_tls_private_key(self, event: ActionEvent) -> None:
"""Set the TLS private key, which will be used for requesting the certificate."""
self._request_certificate(event.params.get("private-key", None))

def _request_certificate(self, param: Optional[str]):
"""Request a certificate to TLS Certificates Operator."""
if param is None:
key = generate_private_key()
else:
key = self._parse_tls_file(param)

csr = generate_csr(
private_key=key,
subject=self.charm.get_hostname_by_unit(self.charm.unit.name),
**self._get_sans(),
)

self.charm.set_secret(SCOPE, "key", key.decode("utf-8"))
self.charm.set_secret(SCOPE, "csr", csr.decode("utf-8"))

if self.charm.model.get_relation(TLS_RELATION):
self.certs.request_certificate_creation(certificate_signing_request=csr)

@staticmethod
def _parse_tls_file(raw_content: str) -> bytes:
"""Parse TLS files from both plain text or base64 format."""
plain_text_tls_file_regex = r"(-+(BEGIN|END) [A-Z ]+-+)"
if re.match(plain_text_tls_file_regex, raw_content):
return re.sub(
plain_text_tls_file_regex,
"\\1",
raw_content,
).encode("utf-8")
return base64.b64decode(raw_content)

def _on_tls_relation_joined(self, _) -> None:
"""Request certificate when TLS relation joined."""
self._request_certificate(None)

def _on_tls_relation_broken(self, event: RelationBrokenEvent) -> None:
"""Disable TLS when TLS relation broken."""
self.charm.set_secret(SCOPE, "ca", None)
self.charm.set_secret(SCOPE, "cert", None)
self.charm.set_secret(SCOPE, "chain", None)
if not self.charm.update_config():
logger.debug("Cannot update config at this moment")
event.defer()

def _on_certificate_available(self, event: CertificateAvailableEvent) -> None:
"""Enable TLS when TLS certificate available."""
if (
event.certificate_signing_request.strip()
!= str(self.charm.get_secret(SCOPE, "csr")).strip()
):
logger.error("An unknown certificate available.")
return

self.charm.set_secret(
SCOPE, "chain", "\n".join(event.chain) if event.chain is not None else None
)
self.charm.set_secret(SCOPE, "cert", event.certificate)
self.charm.set_secret(SCOPE, "ca", event.ca)

try:
if not self.charm.push_tls_files_to_workload():
logger.debug("Cannot push TLS certificates at this moment")
event.defer()
return
except (ConnectionError, PathError, ProtocolError) as e:
logger.error("Cannot push TLS certificates: %r", e)
event.defer()
return

def _on_certificate_expiring(self, event: CertificateExpiringEvent) -> None:
"""Request the new certificate when old certificate is expiring."""
if event.certificate.strip() != str(self.charm.get_secret(SCOPE, "cert")).strip():
logger.error("An unknown certificate expiring.")
return

key = self.charm.get_secret(SCOPE, "key").encode("utf-8")
old_csr = self.charm.get_secret(SCOPE, "csr").encode("utf-8")
new_csr = generate_csr(
private_key=key,
subject=self.charm.get_hostname_by_unit(self.charm.unit.name),
**self._get_sans(),
)
self.certs.request_certificate_renewal(
old_certificate_signing_request=old_csr,
new_certificate_signing_request=new_csr,
)
self.charm.set_secret(SCOPE, "csr", new_csr.decode("utf-8"))

def _get_sans(self) -> dict:
"""Create a list of Subject Alternative Names for a PostgreSQL unit.

Returns:
A list representing the IP and hostnames of the PostgreSQL unit.
"""

def is_ip_address(address: str) -> bool:
"""Returns whether and address is an IP address."""
try:
ipaddress.ip_address(address)
return True
except (ipaddress.AddressValueError, ValueError):
return False

unit_id = self.charm.unit.name.split("/")[1]

# Create a list of all the Subject Alternative Names.
sans = [
f"{self.charm.app.name}-{unit_id}",
self.charm.get_hostname_by_unit(self.charm.unit.name),
socket.getfqdn(),
str(self.charm.model.get_binding(self.peer_relation).network.bind_address),
]
sans.extend(self.additional_dns_names)

# Separate IP addresses and DNS names.
sans_ip = [san for san in sans if is_ip_address(san)]
# IP address need to be part of the DNS SANs list due to
# https://github.com/pgbackrest/pgbackrest/issues/1977.
sans_dns = sans

return {
"sans_ip": sans_ip,
"sans_dns": sans_dns,
}

def get_tls_files(self) -> (Optional[str], Optional[str], Optional[str]):
"""Prepare TLS files in special PostgreSQL way.

PostgreSQL needs three files:
— CA file should have a full chain.
— Key file should have private key.
— Certificate file should have certificate without certificate chain.
"""
ca = self.charm.get_secret(SCOPE, "ca")
chain = self.charm.get_secret(SCOPE, "chain")
ca_file = chain if chain else ca

key = self.charm.get_secret(SCOPE, "key")
cert = self.charm.get_secret(SCOPE, "cert")
return key, ca_file, cert
Loading
Loading