Merge ~pjdc/charm-k8s-ingress/+git/charm-k8s-ingress:automatic-ingress-class into charm-k8s-ingress:master

Proposed by Paul Collins
Status: Merged
Approved by: Tom Haddon
Approved revision: 993dc3bcd049859d173ac31f348380ed370fcc37
Merged at revision: 0590bdb95c2c91f838c820c1c4063204cbfebef3
Proposed branch: ~pjdc/charm-k8s-ingress/+git/charm-k8s-ingress:automatic-ingress-class
Merge into: charm-k8s-ingress:master
Diff against target: 230 lines (+155/-13)
4 files modified
config.yaml (+19/-6)
requirements.txt (+3/-1)
src/charm.py (+30/-4)
tests/unit/test_charm.py (+103/-2)
Reviewer Review Type Date Requested Status
Tom Haddon Approve
Paul Collins Needs Resubmitting
Jon Seager Approve
Review via email: mp+402302@code.launchpad.net

Commit message

when ingress-class is empty, look up the cluster's default class

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
Paul Collins (pjdc) wrote :

Since microk8s changed its default ingress name to "public", and the nginx controller hard-codes its default to "nginx", something like this makes for a more pleasant experience when getting started with this charm.

 * https://github.com/ubuntu/microk8s/issues/2035
 * https://kubernetes.github.io/ingress-nginx/user-guide/multiple-ingress/#multiple-ingress-nginx-controllers

Revision history for this message
Jon Seager (jnsgruk) wrote :

Looks good, a couple of minor nits that are in no way blocking. Thanks! :)

review: Approve
Revision history for this message
Tom Haddon (mthaddon) wrote :

This is a really nice change, but a few comments inline about some changes that would be good to include.

Revision history for this message
Paul Collins (pjdc) wrote :

Addressed all, I believe! Please take a look.

review: Needs Resubmitting
Revision history for this message
Tom Haddon (mthaddon) wrote :

Looks great, thanks very much!

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

Change successfully merged at revision 0590bdb95c2c91f838c820c1c4063204cbfebef3

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/config.yaml b/config.yaml
2index edaed4c..e15b8ea 100644
3--- a/config.yaml
4+++ b/config.yaml
5@@ -4,12 +4,25 @@ options:
6 ingress-class:
7 default: ""
8 description: >
9- The ingress class annotation to target for this ingress resource. If your
10- kubernetes cluster has multiple ingress controllers, this allows you to
11- target the correct one. See https://kubernetes.github.io/ingress-nginx/user-guide/multiple-ingress/
12- for more details. Leaving empty will mean the "kubernetes.io/ingress.class"
13- annotation is unset. This value isn't available to be set via the relation
14- as it's a deploy-time configuration item.
15+ The ingress class to target for this ingress resource.
16+
17+ If your cluster has multiple ingress controllers, this allows
18+ you to select the correct one, by setting the ingressClassName
19+ field on the ingress resource created by the charm.
20+
21+ This value isn't available to be set via the relation as it's a
22+ property of the cluster's configuration.
23+
24+ If this value is empty, the charm will use whichever ingress class has the
25+ "ingressclass.kubernetes.io/is-default-class" annotation set to "true".
26+
27+ If multiple ingress classes are so configured, no selection will be made.
28+
29+ For more details, see:
30+
31+ * https://kubernetes.github.io/ingress-nginx/user-guide/multiple-ingress/
32+
33+ * https://kubernetes.io/docs/concepts/services-networking/ingress/#default-ingress-class
34 type: string
35 limit-rps:
36 default: 0
37diff --git a/requirements.txt b/requirements.txt
38index cd914f9..05a91aa 100644
39--- a/requirements.txt
40+++ b/requirements.txt
41@@ -1,2 +1,4 @@
42-kubernetes
43+# IngressClass resources are not supported by the current
44+# release, so select the latest known pre-release version.
45+kubernetes == 18.17.0a1
46 ops
47diff --git a/src/charm.py b/src/charm.py
48index c1a9481..72cb7d2 100755
49--- a/src/charm.py
50+++ b/src/charm.py
51@@ -247,10 +247,6 @@ class NginxIngressCharm(CharmBase):
52 else:
53 annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
54
55- # Run-time configuration options.
56- if self.config["ingress-class"]:
57- annotations["kubernetes.io/ingress.class"] = self.config["ingress-class"]
58-
59 return kubernetes.client.NetworkingV1beta1Ingress(
60 api_version="networking.k8s.io/v1beta1",
61 kind="Ingress",
62@@ -298,11 +294,41 @@ class NginxIngressCharm(CharmBase):
63 self._service_name,
64 )
65
66+ def _look_up_and_set_ingress_class(self, api, body):
67+ """Set the configured ingress class, otherwise the cluster's default ingress class."""
68+ ingress_class = self.config['ingress-class']
69+ if not ingress_class:
70+ defaults = [
71+ item.metadata.name
72+ for item in api.list_ingress_class().items
73+ if item.metadata.annotations.get('ingressclass.kubernetes.io/is-default-class')
74+ == 'true'
75+ ]
76+
77+ if not defaults:
78+ logger.warning("Cluster has no default ingress class defined")
79+ return
80+
81+ if len(defaults) > 1:
82+ logger.warning(
83+ "Multiple default ingress classes defined, declining to choose between them. "
84+ "They are: {}".format(' '.join(sorted(defaults)))
85+ )
86+ return
87+
88+ ingress_class = defaults[0]
89+ logger.info(
90+ "Using ingress class {} as it is the cluster's default".format(ingress_class)
91+ )
92+
93+ body.spec.ingress_class_name = ingress_class
94+
95 def _define_ingress(self):
96 """Create or update an ingress in kubernetes."""
97 self.k8s_auth()
98 api = _networking_v1_beta1_api()
99 body = self._get_k8s_ingress()
100+ self._look_up_and_set_ingress_class(api, body)
101 ingresses = api.list_namespaced_ingress(namespace=self._namespace)
102 if self._ingress_name in [x.metadata.name for x in ingresses.items]:
103 api.replace_namespaced_ingress(
104diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py
105index d5da7b4..2d9b816 100644
106--- a/tests/unit/test_charm.py
107+++ b/tests/unit/test_charm.py
108@@ -3,7 +3,7 @@
109
110 import unittest
111
112-from unittest.mock import patch
113+from unittest.mock import MagicMock, patch
114
115 import kubernetes
116
117@@ -359,7 +359,6 @@ class TestCharm(unittest.TestCase):
118 metadata=kubernetes.client.V1ObjectMeta(
119 name="gunicorn-ingress",
120 annotations={
121- "kubernetes.io/ingress.class": "nginx",
122 "nginx.ingress.kubernetes.io/affinity": "cookie",
123 "nginx.ingress.kubernetes.io/affinity-mode": "balanced",
124 "nginx.ingress.kubernetes.io/proxy-body-size": "20m",
125@@ -460,3 +459,105 @@ class TestCharm(unittest.TestCase):
126 ),
127 )
128 self.assertEqual(self.harness.charm._get_k8s_service(), expected)
129+
130+
131+INGRESS_CLASS_PUBLIC_DEFAULT = kubernetes.client.V1beta1IngressClass(
132+ metadata=kubernetes.client.V1ObjectMeta(
133+ annotations={
134+ 'ingressclass.kubernetes.io/is-default-class': 'true',
135+ },
136+ name='public',
137+ ),
138+ spec=kubernetes.client.V1beta1IngressClassSpec(
139+ controller='k8s.io/ingress-nginx',
140+ ),
141+)
142+
143+INGRESS_CLASS_PRIVATE = kubernetes.client.V1beta1IngressClass(
144+ metadata=kubernetes.client.V1ObjectMeta(
145+ annotations={},
146+ name='private',
147+ ),
148+ spec=kubernetes.client.V1beta1IngressClassSpec(
149+ controller='k8s.io/ingress-nginx',
150+ ),
151+)
152+
153+INGRESS_CLASS_PRIVATE_DEFAULT = kubernetes.client.V1beta1IngressClass(
154+ metadata=kubernetes.client.V1ObjectMeta(
155+ annotations={
156+ 'ingressclass.kubernetes.io/is-default-class': 'true',
157+ },
158+ name='private',
159+ ),
160+ spec=kubernetes.client.V1beta1IngressClassSpec(
161+ controller='k8s.io/ingress-nginx',
162+ ),
163+)
164+
165+ZERO_INGRESS_CLASS_LIST = kubernetes.client.V1beta1IngressClassList(items=[])
166+
167+ONE_INGRESS_CLASS_LIST = kubernetes.client.V1beta1IngressClassList(
168+ items=[
169+ INGRESS_CLASS_PUBLIC_DEFAULT,
170+ ],
171+)
172+
173+TWO_INGRESS_CLASSES_LIST = kubernetes.client.V1beta1IngressClassList(
174+ items=[
175+ INGRESS_CLASS_PUBLIC_DEFAULT,
176+ INGRESS_CLASS_PRIVATE,
177+ ]
178+)
179+
180+TWO_INGRESS_CLASSES_LIST_TWO_DEFAULT = kubernetes.client.V1beta1IngressClassList(
181+ items=[
182+ INGRESS_CLASS_PUBLIC_DEFAULT,
183+ INGRESS_CLASS_PRIVATE_DEFAULT,
184+ ]
185+)
186+
187+
188+def _make_mock_api_list_ingress_class(return_value):
189+ mock_api = MagicMock()
190+ mock_api.list_ingress_class.return_value = return_value
191+ return mock_api
192+
193+
194+class TestCharmLookUpAndSetIngressClass(unittest.TestCase):
195+ def setUp(self):
196+ self.harness = Harness(NginxIngressCharm)
197+ self.addCleanup(self.harness.cleanup)
198+ self.harness.begin()
199+ self.harness.disable_hooks()
200+ self.harness.update_config(
201+ {"service-hostname": "foo.internal", "service-name": "gunicorn", "service-port": 80}
202+ )
203+
204+ def test_zero_ingress_class(self):
205+ """If there are no ingress classes, there's nothing to choose from."""
206+ api = _make_mock_api_list_ingress_class(ZERO_INGRESS_CLASS_LIST)
207+ body = self.harness.charm._get_k8s_ingress()
208+ self.harness.charm._look_up_and_set_ingress_class(api, body)
209+ self.assertIsNone(body.spec.ingress_class_name)
210+
211+ def test_one_ingress_class(self):
212+ """If there's one default ingress class, choose that."""
213+ api = _make_mock_api_list_ingress_class(ONE_INGRESS_CLASS_LIST)
214+ body = self.harness.charm._get_k8s_ingress()
215+ self.harness.charm._look_up_and_set_ingress_class(api, body)
216+ self.assertEqual(body.spec.ingress_class_name, 'public')
217+
218+ def test_two_ingress_classes(self):
219+ """If there are two ingress classes, one default, choose that."""
220+ api = _make_mock_api_list_ingress_class(TWO_INGRESS_CLASSES_LIST)
221+ body = self.harness.charm._get_k8s_ingress()
222+ self.harness.charm._look_up_and_set_ingress_class(api, body)
223+ self.assertEqual(body.spec.ingress_class_name, 'public')
224+
225+ def test_two_ingress_classes_two_default(self):
226+ """If there are two ingress classes, both default, choose neither."""
227+ api = _make_mock_api_list_ingress_class(TWO_INGRESS_CLASSES_LIST_TWO_DEFAULT)
228+ body = self.harness.charm._get_k8s_ingress()
229+ self.harness.charm._look_up_and_set_ingress_class(api, body)
230+ self.assertIsNone(body.spec.ingress_class_name)

Subscribers

People subscribed via source and target branches

to all changes: