Merge ~mthaddon/charm-k8s-ingress/+git/charm-k8s-ingress:config-changed-unittest into charm-k8s-ingress:master

Proposed by Tom Haddon
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)
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

To post a comment you must log in.
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

This merge proposal is being monitored by mergebot. Change the status to Approved to merge.

Revision history for this message
🤖 prod-jenkaas-is (prod-jenkaas-is) wrote :

A CI job is currently in progress. A follow up comment will be added when it completes.

Revision history for this message
🤖 prod-jenkaas-is (prod-jenkaas-is) wrote :

FAILED: Continuous integration, rev:f6938cd2024c1d2412fdce4268c1941715ca8af8
https://jenkins.canonical.com/is/job/lp-charm-k8s-ingress-ci/1/
Executed test runs:
    FAILURE: https://jenkins.canonical.com/is/job/lp-charm-test/31/
    None: https://jenkins.canonical.com/is/job/lp-update-mp/49054/

Click here to trigger a rebuild:
https://jenkins.canonical.com/is/job/lp-charm-k8s-ingress-ci/1//rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
🤖 prod-jenkaas-is (prod-jenkaas-is) wrote :

A CI job is currently in progress. A follow up comment will be added when it completes.

Revision history for this message
🤖 prod-jenkaas-is (prod-jenkaas-is) wrote :

PASSED: Continuous integration, rev:7fea2aecaf46a1927e822172ef888f6128b0ab1e
https://jenkins.canonical.com/is/job/lp-charm-k8s-ingress-ci/2/
Executed test runs:
    SUCCESS: https://jenkins.canonical.com/is/job/lp-charm-test/32/
    None: https://jenkins.canonical.com/is/job/lp-update-mp/49069/

Click here to trigger a rebuild:
https://jenkins.canonical.com/is/job/lp-charm-k8s-ingress-ci/2//rebuild

review: Approve (continuous-integration)
Revision history for this message
🤖 prod-jenkaas-is (prod-jenkaas-is) wrote :

A CI job is currently in progress. A follow up comment will be added when it completes.

Revision history for this message
🤖 prod-jenkaas-is (prod-jenkaas-is) wrote :

PASSED: Continuous integration, rev:4a62f3ec695281044454c9fd22686b53728ec76b
https://jenkins.canonical.com/is/job/lp-charm-k8s-ingress-ci/3/
Executed test runs:
    SUCCESS: https://jenkins.canonical.com/is/job/lp-charm-test/33/
    None: https://jenkins.canonical.com/is/job/lp-update-mp/49105/

Click here to trigger a rebuild:
https://jenkins.canonical.com/is/job/lp-charm-k8s-ingress-ci/3//rebuild

review: Approve (continuous-integration)
Revision history for this message
Jon Seager (jnsgruk) wrote :

Looks good to me - nice progress! :)

review: Approve
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

Change successfully merged at revision f5ac67a015852f341d978192f4b3efc78bcaa6e9

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/Makefile b/Makefile
2index 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
14diff --git a/config.yaml b/config.yaml
15index 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
26diff --git a/metadata.yaml b/metadata.yaml
27index 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:
44diff --git a/src/charm.py b/src/charm.py
45index 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__":
236diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py
237index 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)

Subscribers

People subscribed via source and target branches

to all changes: