Skip to content

Commit 17e78ea

Browse files
committed
MK8S-184 - Use modern Python library pathlib.Path instead of os.path
1 parent ab2f406 commit 17e78ea

File tree

2 files changed

+106
-94
lines changed

2 files changed

+106
-94
lines changed

salt/metalk8s/addons/prometheus-operator/deployed/files/restart-on-ca-change.py

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,25 @@
33
import os
44
import sys
55
from datetime import datetime, timezone
6+
from pathlib import Path
67

78
import requests
89

910
HASH_FILE_NAME = ".ca-hash-previous"
1011

11-
SA_TOKEN = "/var/run/secrets/kubernetes.io/serviceaccount/token"
12-
SA_CA = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
12+
SA_TOKEN = Path("/var/run/secrets/kubernetes.io/serviceaccount/token")
13+
SA_CA = Path("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt")
1314
K8S_API = "https://kubernetes.default.svc"
1415

1516

16-
def hash_file(file_path: str) -> str:
17+
def hash_file(file_path: Path) -> str:
1718
h = hashlib.sha256()
18-
with open(file_path, "rb") as f:
19-
h.update(f.read())
19+
h.update(file_path.read_bytes())
2020
return h.hexdigest()
2121

2222

2323
def trigger_restart(namespace: str, deployment: str) -> None:
24-
with open(SA_TOKEN) as f:
25-
token = f.read()
24+
token = SA_TOKEN.read_text()
2625
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
2726
body = {
2827
"spec": {
@@ -47,24 +46,22 @@ def trigger_restart(namespace: str, deployment: str) -> None:
4746

4847

4948
def main() -> None:
50-
ca_dir = os.environ["CA_DIR"]
51-
ca_file = os.path.join(ca_dir, os.environ["CA_FILE_NAME"])
52-
hash_file_path = os.path.join(ca_dir, HASH_FILE_NAME)
49+
ca_dir = Path(os.environ["CA_DIR"])
50+
ca_file = ca_dir / os.environ["CA_FILE_NAME"]
51+
hash_file_path = ca_dir / HASH_FILE_NAME
5352

54-
if not os.path.exists(ca_file):
53+
if not ca_file.exists():
5554
print(f"CA file {ca_file} does not exist, skipping")
5655
return
5756

5857
current_hash = hash_file(ca_file)
5958

60-
if not os.path.exists(hash_file_path):
61-
with open(hash_file_path, "w") as f:
62-
f.write(current_hash)
59+
if not hash_file_path.exists():
60+
hash_file_path.write_text(current_hash)
6361
print("Initial CA load, skipping restart")
6462
return
6563

66-
with open(hash_file_path) as f:
67-
previous_hash = f.read().strip()
64+
previous_hash = hash_file_path.read_text().strip()
6865

6966
if current_hash == previous_hash:
7067
return
@@ -82,8 +79,7 @@ def main() -> None:
8279
sys.exit(1)
8380

8481
# Persist hash only after successful restart
85-
with open(hash_file_path, "w") as f:
86-
f.write(current_hash)
82+
hash_file_path.write_text(current_hash)
8783
print(f"Rolling restart triggered for {deployment}")
8884

8985

salt/tests/unit/scripts/test_restart_on_ca_change.py

Lines changed: 92 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
import importlib.util
44
import os
5+
import tempfile
6+
from pathlib import Path
57

68
import requests
79
from unittest import TestCase
8-
from unittest.mock import mock_open, patch
10+
from unittest.mock import patch
911

1012
# The script has a hyphenated filename, so we need importlib to load it
1113
_SCRIPT_PATH = os.path.join(
@@ -37,28 +39,36 @@ class TestHashFile(TestCase):
3739

3840
def test_returns_sha256_hex(self):
3941
"""hash_file returns a 64-char hex string."""
40-
with patch("builtins.open", mock_open(read_data=b"cert-data")):
41-
result = restart_on_ca_change.hash_file("/tmp/secrets/ca.crt")
42+
with tempfile.NamedTemporaryFile() as f:
43+
f.write(b"cert-data")
44+
f.flush()
45+
result = restart_on_ca_change.hash_file(Path(f.name))
4246
self.assertIsInstance(result, str)
4347
self.assertEqual(len(result), 64)
4448

4549
def test_different_content_different_hash(self):
4650
"""hash_file returns different hashes for different file contents."""
47-
with patch("builtins.open", mock_open(read_data=b"cert-v1")):
48-
hash_v1 = restart_on_ca_change.hash_file("/tmp/secrets/ca.crt")
51+
with tempfile.NamedTemporaryFile() as f1, tempfile.NamedTemporaryFile() as f2:
52+
f1.write(b"cert-v1")
53+
f1.flush()
54+
hash_v1 = restart_on_ca_change.hash_file(Path(f1.name))
4955

50-
with patch("builtins.open", mock_open(read_data=b"cert-v2")):
51-
hash_v2 = restart_on_ca_change.hash_file("/tmp/secrets/ca.crt")
56+
f2.write(b"cert-v2")
57+
f2.flush()
58+
hash_v2 = restart_on_ca_change.hash_file(Path(f2.name))
5259

5360
self.assertNotEqual(hash_v1, hash_v2)
5461

5562
def test_same_content_same_hash(self):
5663
"""hash_file returns the same hash for the same content."""
57-
with patch("builtins.open", mock_open(read_data=b"cert-data")):
58-
hash_1 = restart_on_ca_change.hash_file("/tmp/secrets/ca.crt")
64+
with tempfile.NamedTemporaryFile() as f1, tempfile.NamedTemporaryFile() as f2:
65+
f1.write(b"cert-data")
66+
f1.flush()
67+
hash_1 = restart_on_ca_change.hash_file(Path(f1.name))
5968

60-
with patch("builtins.open", mock_open(read_data=b"cert-data")):
61-
hash_2 = restart_on_ca_change.hash_file("/tmp/secrets/ca.crt")
69+
f2.write(b"cert-data")
70+
f2.flush()
71+
hash_2 = restart_on_ca_change.hash_file(Path(f2.name))
6272

6373
self.assertEqual(hash_1, hash_2)
6474

@@ -67,49 +77,54 @@ def test_same_content_same_hash(self):
6777
class TestMain(TestCase):
6878
"""Tests for main function."""
6979

70-
@patch("os.path.exists", return_value=False)
71-
def test_ca_file_missing_skips(self, _mock_exists):
80+
def test_ca_file_missing_skips(self):
7281
"""main skips when CA file does not exist."""
73-
with patch("builtins.print") as mock_print:
74-
restart_on_ca_change.main()
75-
mock_print.assert_called_once_with(
76-
"CA file /tmp/secrets/ca.crt does not exist, skipping"
77-
)
82+
with tempfile.TemporaryDirectory() as tmpdir:
83+
env = {**ENV_VARS, "CA_DIR": tmpdir}
84+
with patch.dict(os.environ, env), patch("builtins.print") as mock_print:
85+
restart_on_ca_change.main()
86+
mock_print.assert_called_once_with(
87+
f"CA file {Path(tmpdir) / 'ca.crt'} does not exist, skipping"
88+
)
7889

7990
def test_initial_load_skips_restart(self):
8091
"""main writes hash and skips restart on initial load."""
81-
# ca_file exists, hash_file_path does not
82-
with patch(
83-
"os.path.exists", side_effect=lambda p: p == "/tmp/secrets/ca.crt"
84-
), patch.object(
85-
restart_on_ca_change, "hash_file", return_value="abc123"
86-
), patch(
87-
"builtins.open", mock_open()
88-
), patch(
89-
"builtins.print"
90-
) as mock_print:
91-
restart_on_ca_change.main()
92-
mock_print.assert_called_once_with("Initial CA load, skipping restart")
92+
with tempfile.TemporaryDirectory() as tmpdir:
93+
ca_file = Path(tmpdir) / "ca.crt"
94+
ca_file.write_bytes(b"cert-data")
95+
env = {**ENV_VARS, "CA_DIR": tmpdir}
96+
with patch.dict(os.environ, env), patch("builtins.print") as mock_print:
97+
restart_on_ca_change.main()
98+
mock_print.assert_called_once_with("Initial CA load, skipping restart")
99+
# Hash file should have been created
100+
hash_file = Path(tmpdir) / ".ca-hash-previous"
101+
self.assertTrue(hash_file.exists())
93102

94103
def test_unchanged_hash_no_restart(self):
95104
"""main does nothing when hash has not changed."""
96-
with patch("os.path.exists", return_value=True), patch.object(
97-
restart_on_ca_change, "hash_file", return_value="same-hash"
98-
), patch("builtins.open", mock_open(read_data="same-hash")), patch(
99-
"builtins.print"
100-
) as mock_print:
101-
restart_on_ca_change.main()
102-
mock_print.assert_not_called()
105+
with tempfile.TemporaryDirectory() as tmpdir:
106+
ca_file = Path(tmpdir) / "ca.crt"
107+
ca_file.write_bytes(b"cert-data")
108+
# Pre-compute and write the hash
109+
current_hash = restart_on_ca_change.hash_file(ca_file)
110+
hash_file = Path(tmpdir) / ".ca-hash-previous"
111+
hash_file.write_text(current_hash)
112+
env = {**ENV_VARS, "CA_DIR": tmpdir}
113+
with patch.dict(os.environ, env), patch("builtins.print") as mock_print:
114+
restart_on_ca_change.main()
115+
mock_print.assert_not_called()
103116

104117
@patch.object(restart_on_ca_change, "trigger_restart")
105118
def test_changed_hash_triggers_restart(self, mock_restart):
106119
"""main triggers restart when hash has changed."""
107-
with patch("os.path.exists", return_value=True), patch.object(
108-
restart_on_ca_change, "hash_file", return_value="new-hash"
109-
), patch("builtins.open", mock_open(read_data="old-hash")), patch(
110-
"builtins.print"
111-
) as mock_print:
112-
restart_on_ca_change.main()
120+
with tempfile.TemporaryDirectory() as tmpdir:
121+
ca_file = Path(tmpdir) / "ca.crt"
122+
ca_file.write_bytes(b"cert-data")
123+
hash_file = Path(tmpdir) / ".ca-hash-previous"
124+
hash_file.write_text("old-hash")
125+
env = {**ENV_VARS, "CA_DIR": tmpdir}
126+
with patch.dict(os.environ, env), patch("builtins.print") as mock_print:
127+
restart_on_ca_change.main()
113128

114129
mock_restart.assert_called_once_with(
115130
"metalk8s-monitoring", "oauth2-proxy-prometheus"
@@ -119,62 +134,63 @@ def test_changed_hash_triggers_restart(self, mock_restart):
119134
mock_print.call_args[0][0],
120135
)
121136

122-
@patch("builtins.print")
123137
@patch.object(
124138
restart_on_ca_change,
125139
"trigger_restart",
126140
side_effect=requests.RequestException("refused"),
127141
)
128-
@patch.object(restart_on_ca_change, "hash_file", return_value="new-hash")
129-
@patch("os.path.exists", return_value=True)
130-
def test_api_failure_exits_with_error(
131-
self, _mock_exists, _mock_hash, _mock_restart, _mock_print
132-
):
142+
def test_api_failure_exits_with_error(self, _mock_restart):
133143
"""main exits with code 1 when the API call fails."""
134-
with patch("builtins.open", mock_open(read_data="old-hash")):
135-
with self.assertRaises(SystemExit) as ctx:
136-
restart_on_ca_change.main()
144+
with tempfile.TemporaryDirectory() as tmpdir:
145+
ca_file = Path(tmpdir) / "ca.crt"
146+
ca_file.write_bytes(b"cert-data")
147+
hash_file = Path(tmpdir) / ".ca-hash-previous"
148+
hash_file.write_text("old-hash")
149+
env = {**ENV_VARS, "CA_DIR": tmpdir}
150+
with patch.dict(os.environ, env), patch("builtins.print"):
151+
with self.assertRaises(SystemExit) as ctx:
152+
restart_on_ca_change.main()
137153

138154
self.assertEqual(ctx.exception.code, 1)
139155

140-
@patch("builtins.print")
141156
@patch.object(
142157
restart_on_ca_change,
143158
"trigger_restart",
144159
side_effect=requests.RequestException("refused"),
145160
)
146-
@patch.object(restart_on_ca_change, "hash_file", return_value="new-hash")
147-
@patch("os.path.exists", return_value=True)
148-
def test_hash_not_persisted_on_api_failure(
149-
self, _mock_exists, _mock_hash, _mock_restart, _mock_print
150-
):
161+
def test_hash_not_persisted_on_api_failure(self, _mock_restart):
151162
"""Hash file is not updated when the API call fails."""
152-
mock_file = mock_open(read_data="old-hash")
153-
154-
with patch("builtins.open", mock_file):
155-
try:
156-
restart_on_ca_change.main()
157-
except SystemExit:
158-
pass
159-
160-
# Only the hash file read should have happened, no write
161-
write_calls = mock_file().write.call_args_list
162-
self.assertEqual(len(write_calls), 0)
163+
with tempfile.TemporaryDirectory() as tmpdir:
164+
ca_file = Path(tmpdir) / "ca.crt"
165+
ca_file.write_bytes(b"cert-data")
166+
hash_file = Path(tmpdir) / ".ca-hash-previous"
167+
hash_file.write_text("old-hash")
168+
env = {**ENV_VARS, "CA_DIR": tmpdir}
169+
with patch.dict(os.environ, env), patch("builtins.print"):
170+
try:
171+
restart_on_ca_change.main()
172+
except SystemExit:
173+
pass
174+
175+
# Hash file should still contain the old hash
176+
self.assertEqual(hash_file.read_text(), "old-hash")
163177

164178

165179
class TestTriggerRestart(TestCase):
166180
"""Tests for trigger_restart function."""
167181

168182
@patch.object(restart_on_ca_change.requests, "patch")
169-
@patch(
170-
"builtins.open",
171-
mock_open(read_data="fake-token"),
172-
)
173183
def test_sends_patch_request(self, mock_patch):
174184
"""trigger_restart sends a PATCH to the K8s API."""
175-
restart_on_ca_change.trigger_restart(
176-
"metalk8s-monitoring", "oauth2-proxy-prometheus"
177-
)
185+
with tempfile.NamedTemporaryFile(mode="w", suffix=".token") as f:
186+
f.write("fake-token")
187+
f.flush()
188+
with patch.object(
189+
restart_on_ca_change, "SA_TOKEN", Path(f.name)
190+
):
191+
restart_on_ca_change.trigger_restart(
192+
"metalk8s-monitoring", "oauth2-proxy-prometheus"
193+
)
178194

179195
mock_patch.assert_called_once()
180196
args, kwargs = mock_patch.call_args

0 commit comments

Comments
 (0)