Skip to content

Commit 1f8ff44

Browse files
Use TLS CA chain for backups (#484)
Signed-off-by: Marcelo Henrique Neppel <[email protected]>
1 parent f135759 commit 1f8ff44

File tree

3 files changed

+112
-13
lines changed

3 files changed

+112
-13
lines changed

src/backups.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,14 @@ def stanza_name(self) -> str:
8383
"""Stanza name, composed by model and cluster name."""
8484
return f"{self.model.name}.{self.charm.cluster_name}"
8585

86+
@property
87+
def _tls_ca_chain_filename(self) -> str:
88+
"""Returns the path to the TLS CA chain file."""
89+
s3_parameters, _ = self._retrieve_s3_parameters()
90+
if s3_parameters.get("tls-ca-chain") is not None:
91+
return f"{self.charm._storage_path}/pgbackrest-tls-ca-chain.crt"
92+
return ""
93+
8694
def _are_backup_settings_ok(self) -> Tuple[bool, Optional[str]]:
8795
"""Validates whether backup settings are OK."""
8896
if self.model.get_relation(self.relation_name) is None:
@@ -234,7 +242,11 @@ def _create_bucket_if_not_exists(self) -> None:
234242
)
235243

236244
try:
237-
s3 = session.resource("s3", endpoint_url=self._construct_endpoint(s3_parameters))
245+
s3 = session.resource(
246+
"s3",
247+
endpoint_url=self._construct_endpoint(s3_parameters),
248+
verify=(self._tls_ca_chain_filename or None),
249+
)
238250
except ValueError as e:
239251
logger.exception("Failed to create a session '%s' in region=%s.", bucket_name, region)
240252
raise e
@@ -896,6 +908,11 @@ def _render_pgbackrest_conf_file(self) -> bool:
896908
)
897909
return False
898910

911+
if self._tls_ca_chain_filename != "":
912+
self.charm._patroni.render_file(
913+
self._tls_ca_chain_filename, "\n".join(s3_parameters["tls-ca-chain"]), 0o644
914+
)
915+
899916
with open("templates/pgbackrest.conf.j2", "r") as file:
900917
template = Template(file.read())
901918
# Render the template file with the correct values.
@@ -909,6 +926,7 @@ def _render_pgbackrest_conf_file(self) -> bool:
909926
endpoint=s3_parameters["endpoint"],
910927
bucket=s3_parameters["bucket"],
911928
s3_uri_style=s3_parameters["s3-uri-style"],
929+
tls_ca_chain=self._tls_ca_chain_filename,
912930
access_key=s3_parameters["access-key"],
913931
secret_key=s3_parameters["secret-key"],
914932
stanza=self.stanza_name,
@@ -1029,7 +1047,11 @@ def _upload_content_to_s3(
10291047
region_name=s3_parameters["region"],
10301048
)
10311049

1032-
s3 = session.resource("s3", endpoint_url=self._construct_endpoint(s3_parameters))
1050+
s3 = session.resource(
1051+
"s3",
1052+
endpoint_url=self._construct_endpoint(s3_parameters),
1053+
verify=(self._tls_ca_chain_filename or None),
1054+
)
10331055
bucket = s3.Bucket(bucket_name)
10341056

10351057
with tempfile.NamedTemporaryFile() as temp_file:

templates/pgbackrest.conf.j2

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ repo1-s3-region={{ region }}
1111
repo1-s3-endpoint={{ endpoint }}
1212
repo1-s3-bucket={{ bucket }}
1313
repo1-s3-uri-style={{ s3_uri_style }}
14+
{%- if tls_ca_chain != '' %}
15+
repo1-s3-ca-file={{ tls_ca_chain }}
16+
{%- endif %}
1417
repo1-s3-key={{ access_key }}
1518
repo1-s3-key-secret={{ secret_key }}
1619
repo1-block=y

tests/unit/test_backups.py

Lines changed: 85 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,33 @@ def test_stanza_name(harness):
5050
)
5151

5252

53+
def test_tls_ca_chain_filename(harness):
54+
# Test when the TLS CA chain is not available.
55+
tc.assertEqual(
56+
harness.charm.backup._tls_ca_chain_filename,
57+
"",
58+
)
59+
60+
# Test when the TLS CA chain is available.
61+
with harness.hooks_disabled():
62+
remote_application = "s3-integrator"
63+
s3_rel_id = harness.add_relation(S3_PARAMETERS_RELATION, remote_application)
64+
harness.update_relation_data(
65+
s3_rel_id,
66+
remote_application,
67+
{
68+
"bucket": "fake-bucket",
69+
"access-key": "fake-access-key",
70+
"secret-key": "fake-secret-key",
71+
"tls-ca-chain": '["fake-tls-ca-chain"]',
72+
},
73+
)
74+
tc.assertEqual(
75+
harness.charm.backup._tls_ca_chain_filename,
76+
"/var/snap/charmed-postgresql/common/pgbackrest-tls-ca-chain.crt",
77+
)
78+
79+
5380
def test_are_backup_settings_ok(harness):
5481
# Test without S3 relation.
5582
tc.assertEqual(
@@ -401,9 +428,17 @@ def test_construct_endpoint(harness):
401428
)
402429

403430

404-
def test_create_bucket_if_not_exists(harness):
431+
@pytest.mark.parametrize(
432+
"tls_ca_chain_filename",
433+
["", "/var/snap/charmed-postgresql/common/pgbackrest-tls-ca-chain.crt"],
434+
)
435+
def test_create_bucket_if_not_exists(harness, tls_ca_chain_filename):
405436
with (
406437
patch("boto3.session.Session.resource") as _resource,
438+
patch(
439+
"charm.PostgreSQLBackups._tls_ca_chain_filename",
440+
new_callable=PropertyMock(return_value=tls_ca_chain_filename),
441+
) as _tls_ca_chain_filename,
407442
patch("charm.PostgreSQLBackups._retrieve_s3_parameters") as _retrieve_s3_parameters,
408443
):
409444
# Test when there are missing S3 parameters.
@@ -427,11 +462,15 @@ def test_create_bucket_if_not_exists(harness):
427462
harness.charm.backup._create_bucket_if_not_exists()
428463

429464
# Test when the bucket already exists.
465+
_resource.reset_mock()
430466
_resource.side_effect = None
431467
head_bucket = _resource.return_value.Bucket.return_value.meta.client.head_bucket
432468
create = _resource.return_value.Bucket.return_value.create
433469
wait_until_exists = _resource.return_value.Bucket.return_value.wait_until_exists
434470
harness.charm.backup._create_bucket_if_not_exists()
471+
_resource.assert_called_once_with(
472+
"s3", endpoint_url="test-endpoint", verify=(tls_ca_chain_filename or None)
473+
)
435474
head_bucket.assert_called_once()
436475
create.assert_not_called()
437476
wait_until_exists.assert_not_called()
@@ -1482,9 +1521,17 @@ def test_pre_restore_checks(harness):
14821521

14831522

14841523
@patch_network_get(private_address="1.1.1.1")
1485-
def test_render_pgbackrest_conf_file(harness):
1524+
@pytest.mark.parametrize(
1525+
"tls_ca_chain_filename",
1526+
["", "/var/snap/charmed-postgresql/common/pgbackrest-tls-ca-chain.crt"],
1527+
)
1528+
def test_render_pgbackrest_conf_file(harness, tls_ca_chain_filename):
14861529
with (
14871530
patch("charm.Patroni.render_file") as _render_file,
1531+
patch(
1532+
"charm.PostgreSQLBackups._tls_ca_chain_filename",
1533+
new_callable=PropertyMock(return_value=tls_ca_chain_filename),
1534+
) as _tls_ca_chain_filename,
14881535
patch("charm.PostgreSQLBackups._retrieve_s3_parameters") as _retrieve_s3_parameters,
14891536
):
14901537
# Set up a mock for the `open` method, set returned data to postgresql.conf template.
@@ -1513,6 +1560,7 @@ def test_render_pgbackrest_conf_file(harness):
15131560
"region": "us-east-1",
15141561
"s3-uri-style": "path",
15151562
"delete-older-than-days": "30",
1563+
"tls-ca-chain": (["fake-tls-ca-chain"] if tls_ca_chain_filename != "" else ""),
15161564
},
15171565
[],
15181566
)
@@ -1531,6 +1579,7 @@ def test_render_pgbackrest_conf_file(harness):
15311579
endpoint="https://storage.googleapis.com",
15321580
bucket="test-bucket",
15331581
s3_uri_style="path",
1582+
tls_ca_chain=(tls_ca_chain_filename or ""),
15341583
access_key="test-access-key",
15351584
secret_key="test-secret-key",
15361585
stanza=harness.charm.backup.stanza_name,
@@ -1548,11 +1597,16 @@ def test_render_pgbackrest_conf_file(harness):
15481597
tc.assertEqual(mock.call_args_list[0][0], ("templates/pgbackrest.conf.j2", "r"))
15491598

15501599
# Ensure the correct rendered template is sent to _render_file method.
1551-
_render_file.assert_called_once_with(
1552-
"/var/snap/charmed-postgresql/current/etc/pgbackrest/pgbackrest.conf",
1553-
expected_content,
1554-
0o644,
1555-
)
1600+
calls = [
1601+
call(
1602+
"/var/snap/charmed-postgresql/current/etc/pgbackrest/pgbackrest.conf",
1603+
expected_content,
1604+
0o644,
1605+
)
1606+
]
1607+
if tls_ca_chain_filename != "":
1608+
calls.insert(0, call(tls_ca_chain_filename, "fake-tls-ca-chain", 0o644))
1609+
_render_file.assert_has_calls(calls)
15561610

15571611

15581612
@patch_network_get(private_address="1.1.1.1")
@@ -1737,11 +1791,19 @@ def test_start_stop_pgbackrest_service(harness):
17371791
restart.assert_called_once()
17381792

17391793

1740-
def test_upload_content_to_s3(harness):
1794+
@pytest.mark.parametrize(
1795+
"tls_ca_chain_filename",
1796+
["", "/var/snap/charmed-postgresql/common/pgbackrest-tls-ca-chain.crt"],
1797+
)
1798+
def test_upload_content_to_s3(harness, tls_ca_chain_filename):
17411799
with (
17421800
patch("tempfile.NamedTemporaryFile") as _named_temporary_file,
17431801
patch("charm.PostgreSQLBackups._construct_endpoint") as _construct_endpoint,
17441802
patch("boto3.session.Session.resource") as _resource,
1803+
patch(
1804+
"charm.PostgreSQLBackups._tls_ca_chain_filename",
1805+
new_callable=PropertyMock(return_value=tls_ca_chain_filename),
1806+
) as _tls_ca_chain_filename,
17451807
):
17461808
# Set some parameters.
17471809
content = "test-content"
@@ -1764,7 +1826,11 @@ def test_upload_content_to_s3(harness):
17641826
harness.charm.backup._upload_content_to_s3(content, s3_path, s3_parameters),
17651827
False,
17661828
)
1767-
_resource.assert_called_once_with("s3", endpoint_url="https://s3.us-east-1.amazonaws.com")
1829+
_resource.assert_called_once_with(
1830+
"s3",
1831+
endpoint_url="https://s3.us-east-1.amazonaws.com",
1832+
verify=(tls_ca_chain_filename or None),
1833+
)
17681834
_named_temporary_file.assert_not_called()
17691835
upload_file.assert_not_called()
17701836

@@ -1775,7 +1841,11 @@ def test_upload_content_to_s3(harness):
17751841
harness.charm.backup._upload_content_to_s3(content, s3_path, s3_parameters),
17761842
False,
17771843
)
1778-
_resource.assert_called_once_with("s3", endpoint_url="https://s3.us-east-1.amazonaws.com")
1844+
_resource.assert_called_once_with(
1845+
"s3",
1846+
endpoint_url="https://s3.us-east-1.amazonaws.com",
1847+
verify=(tls_ca_chain_filename or None),
1848+
)
17791849
_named_temporary_file.assert_called_once()
17801850
upload_file.assert_called_once_with("/tmp/test-file", "test-path/test-file.")
17811851

@@ -1788,6 +1858,10 @@ def test_upload_content_to_s3(harness):
17881858
harness.charm.backup._upload_content_to_s3(content, s3_path, s3_parameters),
17891859
True,
17901860
)
1791-
_resource.assert_called_once_with("s3", endpoint_url="https://s3.us-east-1.amazonaws.com")
1861+
_resource.assert_called_once_with(
1862+
"s3",
1863+
endpoint_url="https://s3.us-east-1.amazonaws.com",
1864+
verify=(tls_ca_chain_filename or None),
1865+
)
17921866
_named_temporary_file.assert_called_once()
17931867
upload_file.assert_called_once_with("/tmp/test-file", "test-path/test-file.")

0 commit comments

Comments
 (0)