Add Ng-UI sidecar charm
[osm/devops.git] / installers / charm / osm-ng-ui / lib / charms / nginx_ingress_integrator / v0 / ingress.py
1 # See LICENSE file for licensing details.
2 # http://www.apache.org/licenses/LICENSE-2.0
3 """Library for the ingress relation.
4
5 This library contains the Requires and Provides classes for handling
6 the ingress interface.
7
8 Import `IngressRequires` in your charm, with two required options:
9 - "self" (the charm itself)
10 - config_dict
11
12 `config_dict` accepts the following keys:
13 - service-hostname (required)
14 - service-name (required)
15 - service-port (required)
16 - additional-hostnames
17 - limit-rps
18 - limit-whitelist
19 - max-body-size
20 - owasp-modsecurity-crs
21 - path-routes
22 - retry-errors
23 - rewrite-enabled
24 - rewrite-target
25 - service-namespace
26 - session-cookie-max-age
27 - tls-secret-name
28
29 See [the config section](https://charmhub.io/nginx-ingress-integrator/configure) for descriptions
30 of each, along with the required type.
31
32 As an example, add the following to `src/charm.py`:
33 ```
34 from charms.nginx_ingress_integrator.v0.ingress import IngressRequires
35
36 # In your charm's `__init__` method.
37 self.ingress = IngressRequires(self, {"service-hostname": self.config["external_hostname"],
38 "service-name": self.app.name,
39 "service-port": 80})
40
41 # In your charm's `config-changed` handler.
42 self.ingress.update_config({"service-hostname": self.config["external_hostname"]})
43 ```
44 And then add the following to `metadata.yaml`:
45 ```
46 requires:
47 ingress:
48 interface: ingress
49 ```
50 You _must_ register the IngressRequires class as part of the `__init__` method
51 rather than, for instance, a config-changed event handler. This is because
52 doing so won't get the current relation changed event, because it wasn't
53 registered to handle the event (because it wasn't created in `__init__` when
54 the event was fired).
55 """
56
57 import logging
58
59 from ops.charm import CharmEvents
60 from ops.framework import EventBase, EventSource, Object
61 from ops.model import BlockedStatus
62
63 # The unique Charmhub library identifier, never change it
64 LIBID = "db0af4367506491c91663468fb5caa4c"
65
66 # Increment this major API version when introducing breaking changes
67 LIBAPI = 0
68
69 # Increment this PATCH version before using `charmcraft publish-lib` or reset
70 # to 0 if you are raising the major API version
71 LIBPATCH = 10
72
73 logger = logging.getLogger(__name__)
74
75 REQUIRED_INGRESS_RELATION_FIELDS = {
76 "service-hostname",
77 "service-name",
78 "service-port",
79 }
80
81 OPTIONAL_INGRESS_RELATION_FIELDS = {
82 "additional-hostnames",
83 "limit-rps",
84 "limit-whitelist",
85 "max-body-size",
86 "owasp-modsecurity-crs",
87 "path-routes",
88 "retry-errors",
89 "rewrite-target",
90 "rewrite-enabled",
91 "service-namespace",
92 "session-cookie-max-age",
93 "tls-secret-name",
94 }
95
96
97 class IngressAvailableEvent(EventBase):
98 pass
99
100
101 class IngressBrokenEvent(EventBase):
102 pass
103
104
105 class IngressCharmEvents(CharmEvents):
106 """Custom charm events."""
107
108 ingress_available = EventSource(IngressAvailableEvent)
109 ingress_broken = EventSource(IngressBrokenEvent)
110
111
112 class IngressRequires(Object):
113 """This class defines the functionality for the 'requires' side of the 'ingress' relation.
114
115 Hook events observed:
116 - relation-changed
117 """
118
119 def __init__(self, charm, config_dict):
120 super().__init__(charm, "ingress")
121
122 self.framework.observe(charm.on["ingress"].relation_changed, self._on_relation_changed)
123
124 self.config_dict = config_dict
125
126 def _config_dict_errors(self, update_only=False):
127 """Check our config dict for errors."""
128 blocked_message = "Error in ingress relation, check `juju debug-log`"
129 unknown = [
130 x
131 for x in self.config_dict
132 if x not in REQUIRED_INGRESS_RELATION_FIELDS | OPTIONAL_INGRESS_RELATION_FIELDS
133 ]
134 if unknown:
135 logger.error(
136 "Ingress relation error, unknown key(s) in config dictionary found: %s",
137 ", ".join(unknown),
138 )
139 self.model.unit.status = BlockedStatus(blocked_message)
140 return True
141 if not update_only:
142 missing = [x for x in REQUIRED_INGRESS_RELATION_FIELDS if x not in self.config_dict]
143 if missing:
144 logger.error(
145 "Ingress relation error, missing required key(s) in config dictionary: %s",
146 ", ".join(sorted(missing)),
147 )
148 self.model.unit.status = BlockedStatus(blocked_message)
149 return True
150 return False
151
152 def _on_relation_changed(self, event):
153 """Handle the relation-changed event."""
154 # `self.unit` isn't available here, so use `self.model.unit`.
155 if self.model.unit.is_leader():
156 if self._config_dict_errors():
157 return
158 for key in self.config_dict:
159 event.relation.data[self.model.app][key] = str(self.config_dict[key])
160
161 def update_config(self, config_dict):
162 """Allow for updates to relation."""
163 if self.model.unit.is_leader():
164 self.config_dict = config_dict
165 if self._config_dict_errors(update_only=True):
166 return
167 relation = self.model.get_relation("ingress")
168 if relation:
169 for key in self.config_dict:
170 relation.data[self.model.app][key] = str(self.config_dict[key])
171
172
173 class IngressProvides(Object):
174 """This class defines the functionality for the 'provides' side of the 'ingress' relation.
175
176 Hook events observed:
177 - relation-changed
178 """
179
180 def __init__(self, charm):
181 super().__init__(charm, "ingress")
182 # Observe the relation-changed hook event and bind
183 # self.on_relation_changed() to handle the event.
184 self.framework.observe(charm.on["ingress"].relation_changed, self._on_relation_changed)
185 self.framework.observe(charm.on["ingress"].relation_broken, self._on_relation_broken)
186 self.charm = charm
187
188 def _on_relation_changed(self, event):
189 """Handle a change to the ingress relation.
190
191 Confirm we have the fields we expect to receive."""
192 # `self.unit` isn't available here, so use `self.model.unit`.
193 if not self.model.unit.is_leader():
194 return
195
196 ingress_data = {
197 field: event.relation.data[event.app].get(field)
198 for field in REQUIRED_INGRESS_RELATION_FIELDS | OPTIONAL_INGRESS_RELATION_FIELDS
199 }
200
201 missing_fields = sorted(
202 [
203 field
204 for field in REQUIRED_INGRESS_RELATION_FIELDS
205 if ingress_data.get(field) is None
206 ]
207 )
208
209 if missing_fields:
210 logger.error(
211 "Missing required data fields for ingress relation: {}".format(
212 ", ".join(missing_fields)
213 )
214 )
215 self.model.unit.status = BlockedStatus(
216 "Missing fields for ingress: {}".format(", ".join(missing_fields))
217 )
218
219 # Create an event that our charm can use to decide it's okay to
220 # configure the ingress.
221 self.charm.on.ingress_available.emit()
222
223 def _on_relation_broken(self, _):
224 """Handle a relation-broken event in the ingress relation."""
225 if not self.model.unit.is_leader():
226 return
227
228 # Create an event that our charm can use to remove the ingress resource.
229 self.charm.on.ingress_broken.emit()