Skip to content

Commit 60f0f10

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

File tree

2 files changed

+112
-105
lines changed

2 files changed

+112
-105
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: 98 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,22 @@
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
11-
_SCRIPT_PATH = os.path.join(
12-
os.path.dirname(os.path.abspath(__file__)),
13-
os.pardir,
14-
os.pardir,
15-
os.pardir,
16-
"metalk8s",
17-
"addons",
18-
"prometheus-operator",
19-
"deployed",
20-
"files",
21-
"restart-on-ca-change.py",
13+
_SCRIPT_PATH = (
14+
Path(__file__).resolve().parents[3]
15+
/ "metalk8s"
16+
/ "addons"
17+
/ "prometheus-operator"
18+
/ "deployed"
19+
/ "files"
20+
/ "restart-on-ca-change.py"
2221
)
2322
_spec = importlib.util.spec_from_file_location("restart_on_ca_change", _SCRIPT_PATH)
2423
restart_on_ca_change = importlib.util.module_from_spec(_spec)
@@ -37,28 +36,36 @@ class TestHashFile(TestCase):
3736

3837
def test_returns_sha256_hex(self):
3938
"""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")
39+
with tempfile.NamedTemporaryFile() as f:
40+
f.write(b"cert-data")
41+
f.flush()
42+
result = restart_on_ca_change.hash_file(Path(f.name))
4243
self.assertIsInstance(result, str)
4344
self.assertEqual(len(result), 64)
4445

4546
def test_different_content_different_hash(self):
4647
"""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")
48+
with tempfile.NamedTemporaryFile() as f1, tempfile.NamedTemporaryFile() as f2:
49+
f1.write(b"cert-v1")
50+
f1.flush()
51+
hash_v1 = restart_on_ca_change.hash_file(Path(f1.name))
4952

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")
53+
f2.write(b"cert-v2")
54+
f2.flush()
55+
hash_v2 = restart_on_ca_change.hash_file(Path(f2.name))
5256

5357
self.assertNotEqual(hash_v1, hash_v2)
5458

5559
def test_same_content_same_hash(self):
5660
"""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")
61+
with tempfile.NamedTemporaryFile() as f1, tempfile.NamedTemporaryFile() as f2:
62+
f1.write(b"cert-data")
63+
f1.flush()
64+
hash_1 = restart_on_ca_change.hash_file(Path(f1.name))
5965

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")
66+
f2.write(b"cert-data")
67+
f2.flush()
68+
hash_2 = restart_on_ca_change.hash_file(Path(f2.name))
6269

6370
self.assertEqual(hash_1, hash_2)
6471

@@ -67,49 +74,54 @@ def test_same_content_same_hash(self):
6774
class TestMain(TestCase):
6875
"""Tests for main function."""
6976

70-
@patch("os.path.exists", return_value=False)
71-
def test_ca_file_missing_skips(self, _mock_exists):
77+
def test_ca_file_missing_skips(self):
7278
"""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-
)
79+
with tempfile.TemporaryDirectory() as tmpdir:
80+
env = {**ENV_VARS, "CA_DIR": tmpdir}
81+
with patch.dict(os.environ, env), patch("builtins.print") as mock_print:
82+
restart_on_ca_change.main()
83+
mock_print.assert_called_once_with(
84+
f"CA file {Path(tmpdir) / 'ca.crt'} does not exist, skipping"
85+
)
7886

7987
def test_initial_load_skips_restart(self):
8088
"""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")
89+
with tempfile.TemporaryDirectory() as tmpdir:
90+
ca_file = Path(tmpdir) / "ca.crt"
91+
ca_file.write_bytes(b"cert-data")
92+
env = {**ENV_VARS, "CA_DIR": tmpdir}
93+
with patch.dict(os.environ, env), patch("builtins.print") as mock_print:
94+
restart_on_ca_change.main()
95+
mock_print.assert_called_once_with("Initial CA load, skipping restart")
96+
# Hash file should have been created
97+
hash_file = Path(tmpdir) / ".ca-hash-previous"
98+
self.assertTrue(hash_file.exists())
9399

94100
def test_unchanged_hash_no_restart(self):
95101
"""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()
102+
with tempfile.TemporaryDirectory() as tmpdir:
103+
ca_file = Path(tmpdir) / "ca.crt"
104+
ca_file.write_bytes(b"cert-data")
105+
# Pre-compute and write the hash
106+
current_hash = restart_on_ca_change.hash_file(ca_file)
107+
hash_file = Path(tmpdir) / ".ca-hash-previous"
108+
hash_file.write_text(current_hash)
109+
env = {**ENV_VARS, "CA_DIR": tmpdir}
110+
with patch.dict(os.environ, env), patch("builtins.print") as mock_print:
111+
restart_on_ca_change.main()
112+
mock_print.assert_not_called()
103113

104114
@patch.object(restart_on_ca_change, "trigger_restart")
105115
def test_changed_hash_triggers_restart(self, mock_restart):
106116
"""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()
117+
with tempfile.TemporaryDirectory() as tmpdir:
118+
ca_file = Path(tmpdir) / "ca.crt"
119+
ca_file.write_bytes(b"cert-data")
120+
hash_file = Path(tmpdir) / ".ca-hash-previous"
121+
hash_file.write_text("old-hash")
122+
env = {**ENV_VARS, "CA_DIR": tmpdir}
123+
with patch.dict(os.environ, env), patch("builtins.print") as mock_print:
124+
restart_on_ca_change.main()
113125

114126
mock_restart.assert_called_once_with(
115127
"metalk8s-monitoring", "oauth2-proxy-prometheus"
@@ -119,62 +131,61 @@ def test_changed_hash_triggers_restart(self, mock_restart):
119131
mock_print.call_args[0][0],
120132
)
121133

122-
@patch("builtins.print")
123134
@patch.object(
124135
restart_on_ca_change,
125136
"trigger_restart",
126137
side_effect=requests.RequestException("refused"),
127138
)
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-
):
139+
def test_api_failure_exits_with_error(self, _mock_restart):
133140
"""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()
141+
with tempfile.TemporaryDirectory() as tmpdir:
142+
ca_file = Path(tmpdir) / "ca.crt"
143+
ca_file.write_bytes(b"cert-data")
144+
hash_file = Path(tmpdir) / ".ca-hash-previous"
145+
hash_file.write_text("old-hash")
146+
env = {**ENV_VARS, "CA_DIR": tmpdir}
147+
with patch.dict(os.environ, env), patch("builtins.print"):
148+
with self.assertRaises(SystemExit) as ctx:
149+
restart_on_ca_change.main()
137150

138151
self.assertEqual(ctx.exception.code, 1)
139152

140-
@patch("builtins.print")
141153
@patch.object(
142154
restart_on_ca_change,
143155
"trigger_restart",
144156
side_effect=requests.RequestException("refused"),
145157
)
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-
):
158+
def test_hash_not_persisted_on_api_failure(self, _mock_restart):
151159
"""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)
160+
with tempfile.TemporaryDirectory() as tmpdir:
161+
ca_file = Path(tmpdir) / "ca.crt"
162+
ca_file.write_bytes(b"cert-data")
163+
hash_file = Path(tmpdir) / ".ca-hash-previous"
164+
hash_file.write_text("old-hash")
165+
env = {**ENV_VARS, "CA_DIR": tmpdir}
166+
with patch.dict(os.environ, env), patch("builtins.print"):
167+
try:
168+
restart_on_ca_change.main()
169+
except SystemExit:
170+
pass
171+
172+
# Hash file should still contain the old hash
173+
self.assertEqual(hash_file.read_text(), "old-hash")
163174

164175

165176
class TestTriggerRestart(TestCase):
166177
"""Tests for trigger_restart function."""
167178

168179
@patch.object(restart_on_ca_change.requests, "patch")
169-
@patch(
170-
"builtins.open",
171-
mock_open(read_data="fake-token"),
172-
)
173180
def test_sends_patch_request(self, mock_patch):
174181
"""trigger_restart sends a PATCH to the K8s API."""
175-
restart_on_ca_change.trigger_restart(
176-
"metalk8s-monitoring", "oauth2-proxy-prometheus"
177-
)
182+
with tempfile.NamedTemporaryFile(mode="w", suffix=".token") as f:
183+
f.write("fake-token")
184+
f.flush()
185+
with patch.object(restart_on_ca_change, "SA_TOKEN", Path(f.name)):
186+
restart_on_ca_change.trigger_restart(
187+
"metalk8s-monitoring", "oauth2-proxy-prometheus"
188+
)
178189

179190
mock_patch.assert_called_once()
180191
args, kwargs = mock_patch.call_args

0 commit comments

Comments
 (0)