Merge ~mthaddon/charm-k8s-ingress/+git/charm-k8s-ingress:rate-limiting into charm-k8s-ingress:master

Proposed by Tom Haddon
Status: Merged
Approved by: Jon Seager
Approved revision: 173827808d137da0d290e2d55fd861e936520757
Merged at revision: 667d31f13320570bb8e324afe9b103fdb5f8376b
Proposed branch: ~mthaddon/charm-k8s-ingress/+git/charm-k8s-ingress:rate-limiting
Merge into: charm-k8s-ingress:master
Diff against target: 148 lines (+82/-1)
4 files modified
config.yaml (+14/-0)
lib/charms/ingress/v0/ingress.py (+5/-1)
src/charm.py (+18/-0)
tests/unit/test_charm.py (+45/-0)
Reviewer Review Type Date Requested Status
🤖 prod-jenkaas-is (community) continuous-integration Approve
ingress-charmers Pending
Review via email: mp+400954@code.launchpad.net

Commit message

Add rate-limiting

To post a comment you must log in.
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
🤖 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 :

PASSED: Continuous integration, rev:173827808d137da0d290e2d55fd861e936520757
https://jenkins.canonical.com/is/job/lp-charm-k8s-ingress-ci/34/
Executed test runs:
    SUCCESS: https://jenkins.canonical.com/is/job/lp-charm-test/71/
    None: https://jenkins.canonical.com/is/job/lp-update-mp/85424/

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

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

Change successfully merged at revision 667d31f13320570bb8e324afe9b103fdb5f8376b

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 5f0c3a4..885c09b 100644
3--- a/config.yaml
4+++ b/config.yaml
5@@ -15,6 +15,20 @@ options:
6 default: ""
7 description: k8s config. If you're using microk8s, this should be the output of `microk8s config`.
8 type: string
9+ limit-rps:
10+ default: 0
11+ description: >
12+ Number of requests accepted from a given IP each second. The burst limit
13+ is set to this limit multiplied by 5. When clients exceed this limit a
14+ 503 error will be returned.
15+ Setting this to 0 disables rate-limiting.
16+ type: int
17+ limit-whitelist:
18+ default: ""
19+ description: >
20+ If rate-limiting is set, client IP source ranges to be excluded. The value
21+ is a comma-separated list of CIDRs.
22+ type: string
23 max-body-size:
24 default: 0
25 description: Max allowed body-size (for file uploads) in megabytes, set to 0 to disable limits.
26diff --git a/lib/charms/ingress/v0/ingress.py b/lib/charms/ingress/v0/ingress.py
27index acef2c0..e002f18 100644
28--- a/lib/charms/ingress/v0/ingress.py
29+++ b/lib/charms/ingress/v0/ingress.py
30@@ -11,6 +11,8 @@ Import `IngressRequires` in your charm, with two required options:
31 - service-hostname (required)
32 - service-name (required)
33 - service-port (required)
34+ - limit-rps
35+ - limit-whitelist
36 - max_body-size
37 - retry-errors
38 - service-namespace
39@@ -47,7 +49,7 @@ LIBAPI = 0
40
41 # Increment this PATCH version before using `charmcraft push-lib` or reset
42 # to 0 if you are raising the major API version
43-LIBPATCH = 8
44+LIBPATCH = 9
45
46 logger = logging.getLogger(__name__)
47
48@@ -58,6 +60,8 @@ REQUIRED_INGRESS_RELATION_FIELDS = {
49 }
50
51 OPTIONAL_INGRESS_RELATION_FIELDS = {
52+ "limit-rps",
53+ "limit-whitelist",
54 "max-body-size",
55 "retry-errors",
56 "service-namespace",
57diff --git a/src/charm.py b/src/charm.py
58index 9f84ece..3531e9d 100755
59--- a/src/charm.py
60+++ b/src/charm.py
61@@ -83,6 +83,20 @@ class IngressCharm(CharmBase):
62 return "{}-ingress".format(self._get_config_or_relation_data("service-name", ""))
63
64 @property
65+ def _limit_rps(self):
66+ """Return limit-rps value from config or relation."""
67+ limit_rps = self._get_config_or_relation_data("limit-rps", 0)
68+ if limit_rps:
69+ return str(limit_rps)
70+ # Don't return "0" which would evaluate to True.
71+ return ""
72+
73+ @property
74+ def _limit_whitelist(self):
75+ """Return the limit-whitelist value from config or relation."""
76+ return self._get_config_or_relation_data("limit-whitelist", "")
77+
78+ @property
79 def _max_body_size(self):
80 """Return the max-body-size to use for k8s ingress."""
81 max_body_size = self._get_config_or_relation_data("max-body-size", 0)
82@@ -206,6 +220,10 @@ class IngressCharm(CharmBase):
83 annotations = {
84 "nginx.ingress.kubernetes.io/rewrite-target": "/",
85 }
86+ if self._limit_rps:
87+ annotations["nginx.ingress.kubernetes.io/limit-rps"] = self._limit_rps
88+ if self._limit_whitelist:
89+ annotations["nginx.ingress.kubernetes.io/limit-whitelist"] = self._limit_whitelist
90 if self._max_body_size:
91 annotations["nginx.ingress.kubernetes.io/proxy-body-size"] = self._max_body_size
92 if self._retry_errors:
93diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py
94index 9dda0c1..9fa2a77 100644
95--- a/tests/unit/test_charm.py
96+++ b/tests/unit/test_charm.py
97@@ -386,6 +386,51 @@ class TestCharm(unittest.TestCase):
98 ),
99 )
100 self.assertEqual(self.harness.charm._get_k8s_ingress(), expected)
101+ # Test limit-whitelist on its own makes no change.
102+ self.harness.update_config({"limit-whitelist": "10.0.0.0/16"})
103+ self.assertEqual(self.harness.charm._get_k8s_ingress(), expected)
104+ # And if we set limit-rps we get both. Unset other options to minimize output.
105+ self.harness.update_config(
106+ {
107+ "limit-rps": 5,
108+ "ingress-class": "",
109+ "max-body-size": 0,
110+ "retry-errors": "",
111+ "session-cookie-max-age": 0,
112+ }
113+ )
114+ expected = kubernetes.client.NetworkingV1beta1Ingress(
115+ api_version="networking.k8s.io/v1beta1",
116+ kind="Ingress",
117+ metadata=kubernetes.client.V1ObjectMeta(
118+ name="gunicorn-ingress",
119+ annotations={
120+ "nginx.ingress.kubernetes.io/limit-rps": "5",
121+ "nginx.ingress.kubernetes.io/limit-whitelist": "10.0.0.0/16",
122+ "nginx.ingress.kubernetes.io/rewrite-target": "/",
123+ "nginx.ingress.kubernetes.io/ssl-redirect": "false",
124+ },
125+ ),
126+ spec=kubernetes.client.NetworkingV1beta1IngressSpec(
127+ rules=[
128+ kubernetes.client.NetworkingV1beta1IngressRule(
129+ host="foo.internal",
130+ http=kubernetes.client.NetworkingV1beta1HTTPIngressRuleValue(
131+ paths=[
132+ kubernetes.client.NetworkingV1beta1HTTPIngressPath(
133+ path="/",
134+ backend=kubernetes.client.NetworkingV1beta1IngressBackend(
135+ service_port=80,
136+ service_name="gunicorn-service",
137+ ),
138+ )
139+ ]
140+ ),
141+ )
142+ ]
143+ ),
144+ )
145+ self.assertEqual(self.harness.charm._get_k8s_ingress(), expected)
146
147 def test_get_k8s_service(self):
148 """Test getting our definition of a k8s service."""

Subscribers

People subscribed via source and target branches

to all changes: