Merge ~mthaddon/charm-k8s-ingress/+git/charm-k8s-ingress:config-changed-unittest into charm-k8s-ingress:master
- Git
- lp:~mthaddon/charm-k8s-ingress/+git/charm-k8s-ingress
- config-changed-unittest
- Merge into master
Status: | Merged |
---|---|
Approved by: | Tom Haddon |
Approved revision: | 4a62f3ec695281044454c9fd22686b53728ec76b |
Merged at revision: | f5ac67a015852f341d978192f4b3efc78bcaa6e9 |
Proposed branch: | ~mthaddon/charm-k8s-ingress/+git/charm-k8s-ingress:config-changed-unittest |
Merge into: | charm-k8s-ingress:master |
Diff against target: |
340 lines (+157/-73) 5 files modified
Makefile (+1/-1) config.yaml (+0/-4) metadata.yaml (+2/-5) src/charm.py (+71/-56) tests/unit/test_charm.py (+83/-7) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jon Seager | Approve | ||
🤖 prod-jenkaas-is (community) | continuous-integration | Approve | |
Review via email: mp+400126@code.launchpad.net |
Commit message
Remove fake config item, update unit tests, refactor how we generate k8s api calls for easier testing
Description of the change
Remove fake config item, update unit tests, refactor how we generate k8s api calls for easier testing
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote : | # |
🤖 prod-jenkaas-is (prod-jenkaas-is) wrote : | # |
A CI job is currently in progress. A follow up comment will be added when it completes.
🤖 prod-jenkaas-is (prod-jenkaas-is) wrote : | # |
FAILED: Continuous integration, rev:f6938cd2024
https:/
Executed test runs:
FAILURE: https:/
None: https:/
Click here to trigger a rebuild:
https:/
🤖 prod-jenkaas-is (prod-jenkaas-is) wrote : | # |
A CI job is currently in progress. A follow up comment will be added when it completes.
🤖 prod-jenkaas-is (prod-jenkaas-is) wrote : | # |
PASSED: Continuous integration, rev:7fea2aecaf4
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
🤖 prod-jenkaas-is (prod-jenkaas-is) wrote : | # |
A CI job is currently in progress. A follow up comment will be added when it completes.
🤖 prod-jenkaas-is (prod-jenkaas-is) wrote : | # |
PASSED: Continuous integration, rev:4a62f3ec695
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
Jon Seager (jnsgruk) wrote : | # |
Looks good to me - nice progress! :)
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote : | # |
Change successfully merged at revision f5ac67a015852f3
Preview Diff
1 | diff --git a/Makefile b/Makefile |
2 | index a3166a1..2d7e3ee 100644 |
3 | --- a/Makefile |
4 | +++ b/Makefile |
5 | @@ -17,7 +17,7 @@ clean: |
6 | @echo "Cleaning files" |
7 | @git clean -fXd |
8 | |
9 | -mattermost.charm: src/*.py requirements.txt |
10 | +k8s-ingress.charm: src/*.py requirements.txt |
11 | charmcraft build |
12 | |
13 | .PHONY: lint test unittest clean |
14 | diff --git a/config.yaml b/config.yaml |
15 | index b2bd3c8..fe2691d 100644 |
16 | --- a/config.yaml |
17 | +++ b/config.yaml |
18 | @@ -21,7 +21,3 @@ options: |
19 | default: 0 |
20 | description: The port of the service to create an ingress for. |
21 | type: int |
22 | - thing: |
23 | - default: 🎁 |
24 | - description: A thing used by the charm. |
25 | - type: string |
26 | diff --git a/metadata.yaml b/metadata.yaml |
27 | index 6b4c014..f1a62fc 100644 |
28 | --- a/metadata.yaml |
29 | +++ b/metadata.yaml |
30 | @@ -5,11 +5,8 @@ description: | |
31 | An operator to configure a kubernetes ingress. |
32 | summary: | |
33 | An operator to configure a kubernetes ingress. |
34 | -min-juju-version: 2.9.0 |
35 | -platforms: |
36 | - - kubernetes |
37 | -systems: |
38 | - - os: ubuntu |
39 | +bases: |
40 | + - name: ubuntu |
41 | channel: 20.04/stable |
42 | |
43 | containers: |
44 | diff --git a/src/charm.py b/src/charm.py |
45 | index 4b85b26..3bd55ff 100755 |
46 | --- a/src/charm.py |
47 | +++ b/src/charm.py |
48 | @@ -21,7 +21,6 @@ import kubernetes |
49 | from ops.charm import CharmBase |
50 | from ops.main import main |
51 | from ops.model import ActiveStatus |
52 | -from ops.framework import StoredState |
53 | |
54 | logger = logging.getLogger(__name__) |
55 | |
56 | @@ -49,12 +48,22 @@ class CharmK8SIngressCharm(CharmBase): |
57 | """Charm the service.""" |
58 | |
59 | _authed = False |
60 | - _stored = StoredState() |
61 | |
62 | def __init__(self, *args): |
63 | super().__init__(*args) |
64 | self.framework.observe(self.on.config_changed, self._on_config_changed) |
65 | - self._stored.set_default(things=[]) |
66 | + |
67 | + @property |
68 | + def _ingress_name(self): |
69 | + """Return an ingress name for use creating a k8s ingress.""" |
70 | + # Follow the same naming convention as Juju. |
71 | + return "{}-ingress".format(self.config["service-name"]) |
72 | + |
73 | + @property |
74 | + def _service_name(self): |
75 | + """Return a service name for the use creating a k8s service.""" |
76 | + # Avoid collision with service name created by Juju. |
77 | + return "{}-service".format(self.config["service-name"]) |
78 | |
79 | def k8s_auth(self): |
80 | """Authenticate to kubernetes.""" |
81 | @@ -73,19 +82,12 @@ class CharmK8SIngressCharm(CharmBase): |
82 | |
83 | self._authed = True |
84 | |
85 | - def _define_service(self): |
86 | - """Create or update a service in kubernetes.""" |
87 | - self.k8s_auth() |
88 | - api = _core_v1_api() |
89 | - |
90 | - # Juju will create a service named the same as the application. We |
91 | - # need to use a different name to avoid collision. |
92 | - service_name = "{}-service".format(self.config["service-name"]) |
93 | - |
94 | - body = kubernetes.client.V1Service( |
95 | + def _get_k8s_service(self): |
96 | + """Get a K8s service definition.""" |
97 | + return kubernetes.client.V1Service( |
98 | api_version="v1", |
99 | kind="Service", |
100 | - metadata=kubernetes.client.V1ObjectMeta(name=service_name), |
101 | + metadata=kubernetes.client.V1ObjectMeta(name=self._service_name), |
102 | spec=kubernetes.client.V1ServiceSpec( |
103 | selector={"app.kubernetes.io/name": self.config["service-name"]}, |
104 | ports=[ |
105 | @@ -97,8 +99,53 @@ class CharmK8SIngressCharm(CharmBase): |
106 | ], |
107 | ), |
108 | ) |
109 | + |
110 | + def _get_k8s_ingress(self): |
111 | + """Get a K8s ingress definition.""" |
112 | + return kubernetes.client.NetworkingV1beta1Ingress( |
113 | + api_version="networking.k8s.io/v1beta1", |
114 | + kind="Ingress", |
115 | + metadata=kubernetes.client.V1ObjectMeta( |
116 | + name=self._ingress_name, |
117 | + annotations={ |
118 | + "nginx.ingress.kubernetes.io/rewrite-target": "/", |
119 | + "nginx.ingress.kubernetes.io/ssl-redirect": "false", |
120 | + }, |
121 | + ), |
122 | + spec=kubernetes.client.NetworkingV1beta1IngressSpec( |
123 | + rules=[ |
124 | + kubernetes.client.NetworkingV1beta1IngressRule( |
125 | + host=self.config["service-hostname"], |
126 | + http=kubernetes.client.NetworkingV1beta1HTTPIngressRuleValue( |
127 | + paths=[ |
128 | + kubernetes.client.NetworkingV1beta1HTTPIngressPath( |
129 | + path="/", |
130 | + backend=kubernetes.client.NetworkingV1beta1IngressBackend( |
131 | + service_port=self.config["service-port"], |
132 | + service_name=self._service_name, |
133 | + ), |
134 | + ) |
135 | + ] |
136 | + ), |
137 | + ) |
138 | + ] |
139 | + ), |
140 | + ) |
141 | + |
142 | + def _report_service_ips(self): |
143 | + """Report on service IP(s).""" |
144 | + self.k8s_auth() |
145 | + api = _core_v1_api() |
146 | + services = api.list_namespaced_service(namespace=self.config["service-namespace"]) |
147 | + return [x.spec.cluster_ip for x in services.items if x.metadata.name == self._service_name] |
148 | + |
149 | + def _define_service(self): |
150 | + """Create or update a service in kubernetes.""" |
151 | + self.k8s_auth() |
152 | + api = _core_v1_api() |
153 | + body = self._get_k8s_service() |
154 | services = api.list_namespaced_service(namespace=self.config["service-namespace"]) |
155 | - if service_name in [x.metadata.name for x in services.items]: |
156 | + if self._service_name in [x.metadata.name for x in services.items]: |
157 | # Currently failing with port[1].name required but we're only |
158 | # defining one port above... |
159 | # api.patch_namespaced_service( |
160 | @@ -107,7 +154,7 @@ class CharmK8SIngressCharm(CharmBase): |
161 | # body=body, |
162 | # ) |
163 | api.delete_namespaced_service( |
164 | - name=service_name, |
165 | + name=self._service_name, |
166 | namespace=self.config["service-namespace"], |
167 | ) |
168 | api.create_namespaced_service( |
169 | @@ -134,43 +181,11 @@ class CharmK8SIngressCharm(CharmBase): |
170 | """Create or update an ingress in kubernetes.""" |
171 | self.k8s_auth() |
172 | api = _networking_v1_beta1_api() |
173 | - |
174 | - # Follow the same naming convention as Juju. |
175 | - ingress_name = "{}-ingress".format(self.config["service-name"]) |
176 | - |
177 | - body = kubernetes.client.NetworkingV1beta1Ingress( |
178 | - api_version="networking.k8s.io/v1beta1", |
179 | - kind="Ingress", |
180 | - metadata=kubernetes.client.V1ObjectMeta( |
181 | - name=ingress_name, |
182 | - annotations={ |
183 | - "nginx.ingress.kubernetes.io/rewrite-target": "/", |
184 | - "nginx.ingress.kubernetes.io/ssl-redirect": "false", |
185 | - }, |
186 | - ), |
187 | - spec=kubernetes.client.NetworkingV1beta1IngressSpec( |
188 | - rules=[ |
189 | - kubernetes.client.NetworkingV1beta1IngressRule( |
190 | - host=self.config["service-hostname"], |
191 | - http=kubernetes.client.NetworkingV1beta1HTTPIngressRuleValue( |
192 | - paths=[ |
193 | - kubernetes.client.NetworkingV1beta1HTTPIngressPath( |
194 | - path="/", |
195 | - backend=kubernetes.client.NetworkingV1beta1IngressBackend( |
196 | - service_port=self.config["service-port"], |
197 | - service_name="{}-service".format(self.config["service-name"]), |
198 | - ), |
199 | - ) |
200 | - ] |
201 | - ), |
202 | - ) |
203 | - ] |
204 | - ), |
205 | - ) |
206 | + body = self._get_k8s_ingress() |
207 | ingresses = api.list_namespaced_ingress(namespace=self.config["service-namespace"]) |
208 | - if ingress_name in [x.metadata.name for x in ingresses.items]: |
209 | + if self._ingress_name in [x.metadata.name for x in ingresses.items]: |
210 | api.patch_namespaced_ingress( |
211 | - name=ingress_name, |
212 | + name=self._ingress_name, |
213 | namespace=self.config["service-namespace"], |
214 | body=body, |
215 | ) |
216 | @@ -192,14 +207,14 @@ class CharmK8SIngressCharm(CharmBase): |
217 | |
218 | def _on_config_changed(self, _): |
219 | """Handle the config changed event.""" |
220 | - current = self.config["thing"] |
221 | - if current not in self._stored.things: |
222 | - logger.debug("found a new thing: %r", current) |
223 | - self._stored.things.append(current) |
224 | + msg = "" |
225 | if self.config["service-name"]: |
226 | self._define_service() |
227 | self._define_ingress() |
228 | - self.unit.status = ActiveStatus() |
229 | + # It's not recommended to do this via ActiveStatus, but we don't |
230 | + # have another way of reporting status yet. |
231 | + msg = "Ingress with service IP(s): {}".format(", ".join(self._report_service_ips())) |
232 | + self.unit.status = ActiveStatus(msg) |
233 | |
234 | |
235 | if __name__ == "__main__": |
236 | diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py |
237 | index 861051d..2bca57e 100644 |
238 | --- a/tests/unit/test_charm.py |
239 | +++ b/tests/unit/test_charm.py |
240 | @@ -4,17 +4,93 @@ |
241 | import mock |
242 | import unittest |
243 | |
244 | +import kubernetes |
245 | + |
246 | +from ops.model import ActiveStatus |
247 | from ops.testing import Harness |
248 | from charm import CharmK8SIngressCharm |
249 | |
250 | |
251 | class TestCharm(unittest.TestCase): |
252 | + def setUp(self): |
253 | + """Setup the harness object.""" |
254 | + self.harness = Harness(CharmK8SIngressCharm) |
255 | + self.addCleanup(self.harness.cleanup) |
256 | + self.harness.begin() |
257 | + |
258 | + @mock.patch('charm.CharmK8SIngressCharm._report_service_ips') |
259 | @mock.patch('charm.CharmK8SIngressCharm._define_ingress') |
260 | @mock.patch('charm.CharmK8SIngressCharm._define_service') |
261 | - def test_config_changed(self, _define_service, _define_ingress): |
262 | - harness = Harness(CharmK8SIngressCharm) |
263 | - self.addCleanup(harness.cleanup) |
264 | - harness.begin() |
265 | - self.assertEqual(list(harness.charm._stored.things), []) |
266 | - harness.update_config({"thing": "foo"}) |
267 | - self.assertEqual(list(harness.charm._stored.things), ["foo"]) |
268 | + def test_config_changed(self, _define_service, _define_ingress, _report_service_ips): |
269 | + """Test our config changed handler.""" |
270 | + _report_service_ips.return_value = ["10.0.1.12"] |
271 | + # Confirm our _define_ingress and _define_service methods haven't been called. |
272 | + self.assertEqual(_define_ingress.call_count, 0) |
273 | + self.assertEqual(_define_service.call_count, 0) |
274 | + # Test if config-changed is called with service-name empty, our methods still |
275 | + # aren't called. |
276 | + self.harness.update_config({"service-name": ""}) |
277 | + self.assertEqual(_define_ingress.call_count, 0) |
278 | + self.assertEqual(_define_service.call_count, 0) |
279 | + # And now test if we set a service-name config, our methods are called. |
280 | + self.harness.update_config({"service-name": "gunicorn"}) |
281 | + self.assertEqual(_define_ingress.call_count, 1) |
282 | + self.assertEqual(_define_service.call_count, 1) |
283 | + # Confirm status is as expected. |
284 | + self.assertEqual(self.harness.charm.unit.status, ActiveStatus('Ingress with service IP(s): 10.0.1.12')) |
285 | + |
286 | + def test_get_k8s_ingress(self): |
287 | + """Test getting our definition of a k8s ingress.""" |
288 | + self.harness.disable_hooks() |
289 | + self.harness.update_config({"service-name": "gunicorn", "service-port": 80, "service-hostname": "foo.internal"}) |
290 | + expected = kubernetes.client.NetworkingV1beta1Ingress( |
291 | + api_version="networking.k8s.io/v1beta1", |
292 | + kind="Ingress", |
293 | + metadata=kubernetes.client.V1ObjectMeta( |
294 | + name="gunicorn-ingress", |
295 | + annotations={ |
296 | + "nginx.ingress.kubernetes.io/rewrite-target": "/", |
297 | + "nginx.ingress.kubernetes.io/ssl-redirect": "false", |
298 | + }, |
299 | + ), |
300 | + spec=kubernetes.client.NetworkingV1beta1IngressSpec( |
301 | + rules=[ |
302 | + kubernetes.client.NetworkingV1beta1IngressRule( |
303 | + host="foo.internal", |
304 | + http=kubernetes.client.NetworkingV1beta1HTTPIngressRuleValue( |
305 | + paths=[ |
306 | + kubernetes.client.NetworkingV1beta1HTTPIngressPath( |
307 | + path="/", |
308 | + backend=kubernetes.client.NetworkingV1beta1IngressBackend( |
309 | + service_port=80, |
310 | + service_name="gunicorn-service", |
311 | + ), |
312 | + ) |
313 | + ] |
314 | + ), |
315 | + ) |
316 | + ] |
317 | + ), |
318 | + ) |
319 | + self.assertEqual(self.harness.charm._get_k8s_ingress(), expected) |
320 | + |
321 | + def test_get_k8s_service(self): |
322 | + """Test getting our definition of a k8s service.""" |
323 | + self.harness.disable_hooks() |
324 | + self.harness.update_config({"service-name": "gunicorn", "service-port": 80}) |
325 | + expected = kubernetes.client.V1Service( |
326 | + api_version="v1", |
327 | + kind="Service", |
328 | + metadata=kubernetes.client.V1ObjectMeta(name="gunicorn-service"), |
329 | + spec=kubernetes.client.V1ServiceSpec( |
330 | + selector={"app.kubernetes.io/name": "gunicorn"}, |
331 | + ports=[ |
332 | + kubernetes.client.V1ServicePort( |
333 | + name="tcp-80", |
334 | + port=80, |
335 | + target_port=80, |
336 | + ) |
337 | + ], |
338 | + ), |
339 | + ) |
340 | + self.assertEqual(self.harness.charm._get_k8s_service(), expected) |
This merge proposal is being monitored by mergebot. Change the status to Approved to merge.