Adding ImagePullPolicy config option to OSM Charms
[osm/devops.git] / installers / charm / lcm / src / charm.py
1 #!/usr/bin/env python3
2 # Copyright 2021 Canonical Ltd.
3 #
4 # Licensed under the Apache License, Version 2.0 (the "License"); you may
5 # not use this file except in compliance with the License. You may obtain
6 # a copy of the License at
7 #
8 # http://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 # License for the specific language governing permissions and limitations
14 # under the License.
15 #
16 # For those usages not covered by the Apache License, Version 2.0 please
17 # contact: legal@canonical.com
18 #
19 # To get in touch with the maintainers, please contact:
20 # osm-charmers@lists.launchpad.net
21 ##
22
23 # pylint: disable=E0213
24
25
26 import logging
27 from typing import NoReturn, Optional
28
29
30 from ops.main import main
31 from opslib.osm.charm import CharmedOsmBase, RelationsMissing
32 from opslib.osm.interfaces.http import HttpClient
33 from opslib.osm.interfaces.kafka import KafkaClient
34 from opslib.osm.interfaces.mongo import MongoClient
35 from opslib.osm.pod import ContainerV3Builder, PodSpecV3Builder
36 from opslib.osm.validator import ModelValidator, validator
37
38
39 logger = logging.getLogger(__name__)
40
41 PORT = 9999
42
43
44 class ConfigModel(ModelValidator):
45 vca_host: Optional[str]
46 vca_port: Optional[int]
47 vca_user: Optional[str]
48 vca_secret: Optional[str]
49 vca_pubkey: Optional[str]
50 vca_cacert: Optional[str]
51 vca_cloud: Optional[str]
52 vca_k8s_cloud: Optional[str]
53 database_commonkey: str
54 mongodb_uri: Optional[str]
55 log_level: str
56 vca_apiproxy: Optional[str]
57 # Model-config options
58 vca_model_config_agent_metadata_url: Optional[str]
59 vca_model_config_agent_stream: Optional[str]
60 vca_model_config_apt_ftp_proxy: Optional[str]
61 vca_model_config_apt_http_proxy: Optional[str]
62 vca_model_config_apt_https_proxy: Optional[str]
63 vca_model_config_apt_mirror: Optional[str]
64 vca_model_config_apt_no_proxy: Optional[str]
65 vca_model_config_automatically_retry_hooks: Optional[bool]
66 vca_model_config_backup_dir: Optional[str]
67 vca_model_config_cloudinit_userdata: Optional[str]
68 vca_model_config_container_image_metadata_url: Optional[str]
69 vca_model_config_container_image_stream: Optional[str]
70 vca_model_config_container_inherit_properties: Optional[str]
71 vca_model_config_container_networking_method: Optional[str]
72 vca_model_config_default_series: Optional[str]
73 vca_model_config_default_space: Optional[str]
74 vca_model_config_development: Optional[bool]
75 vca_model_config_disable_network_management: Optional[bool]
76 vca_model_config_egress_subnets: Optional[str]
77 vca_model_config_enable_os_refresh_update: Optional[bool]
78 vca_model_config_enable_os_upgrade: Optional[bool]
79 vca_model_config_fan_config: Optional[str]
80 vca_model_config_firewall_mode: Optional[str]
81 vca_model_config_ftp_proxy: Optional[str]
82 vca_model_config_http_proxy: Optional[str]
83 vca_model_config_https_proxy: Optional[str]
84 vca_model_config_ignore_machine_addresses: Optional[bool]
85 vca_model_config_image_metadata_url: Optional[str]
86 vca_model_config_image_stream: Optional[str]
87 vca_model_config_juju_ftp_proxy: Optional[str]
88 vca_model_config_juju_http_proxy: Optional[str]
89 vca_model_config_juju_https_proxy: Optional[str]
90 vca_model_config_juju_no_proxy: Optional[str]
91 vca_model_config_logforward_enabled: Optional[bool]
92 vca_model_config_logging_config: Optional[str]
93 vca_model_config_lxd_snap_channel: Optional[str]
94 vca_model_config_max_action_results_age: Optional[str]
95 vca_model_config_max_action_results_size: Optional[str]
96 vca_model_config_max_status_history_age: Optional[str]
97 vca_model_config_max_status_history_size: Optional[str]
98 vca_model_config_net_bond_reconfigure_delay: Optional[str]
99 vca_model_config_no_proxy: Optional[str]
100 vca_model_config_provisioner_harvest_mode: Optional[str]
101 vca_model_config_proxy_ssh: Optional[bool]
102 vca_model_config_snap_http_proxy: Optional[str]
103 vca_model_config_snap_https_proxy: Optional[str]
104 vca_model_config_snap_store_assertions: Optional[str]
105 vca_model_config_snap_store_proxy: Optional[str]
106 vca_model_config_snap_store_proxy_url: Optional[str]
107 vca_model_config_ssl_hostname_verification: Optional[bool]
108 vca_model_config_test_mode: Optional[bool]
109 vca_model_config_transmit_vendor_metrics: Optional[bool]
110 vca_model_config_update_status_hook_interval: Optional[str]
111 vca_stablerepourl: Optional[str]
112 vca_helm_ca_certs: Optional[str]
113 image_pull_policy: Optional[str]
114
115 @validator("log_level")
116 def validate_log_level(cls, v):
117 if v not in {"INFO", "DEBUG"}:
118 raise ValueError("value must be INFO or DEBUG")
119 return v
120
121 @validator("mongodb_uri")
122 def validate_mongodb_uri(cls, v):
123 if v and not v.startswith("mongodb://"):
124 raise ValueError("mongodb_uri is not properly formed")
125 return v
126
127 @validator("image_pull_policy")
128 def validate_image_pull_policy(cls, v):
129 values = {
130 "always": "Always",
131 "ifnotpresent": "IfNotPresent",
132 "never": "Never",
133 }
134 v = v.lower()
135 if v not in values.keys():
136 raise ValueError("value must be always, ifnotpresent or never")
137 return values[v]
138
139
140 class LcmCharm(CharmedOsmBase):
141 def __init__(self, *args) -> NoReturn:
142 super().__init__(*args, oci_image="image")
143
144 self.kafka_client = KafkaClient(self, "kafka")
145 self.framework.observe(self.on["kafka"].relation_changed, self.configure_pod)
146 self.framework.observe(self.on["kafka"].relation_broken, self.configure_pod)
147
148 self.mongodb_client = MongoClient(self, "mongodb")
149 self.framework.observe(self.on["mongodb"].relation_changed, self.configure_pod)
150 self.framework.observe(self.on["mongodb"].relation_broken, self.configure_pod)
151
152 self.ro_client = HttpClient(self, "ro")
153 self.framework.observe(self.on["ro"].relation_changed, self.configure_pod)
154 self.framework.observe(self.on["ro"].relation_broken, self.configure_pod)
155
156 def _check_missing_dependencies(self, config: ConfigModel):
157 missing_relations = []
158
159 if self.kafka_client.is_missing_data_in_unit():
160 missing_relations.append("kafka")
161 if not config.mongodb_uri and self.mongodb_client.is_missing_data_in_unit():
162 missing_relations.append("mongodb")
163 if self.ro_client.is_missing_data_in_app():
164 missing_relations.append("ro")
165
166 if missing_relations:
167 raise RelationsMissing(missing_relations)
168
169 def build_pod_spec(self, image_info):
170 # Validate config
171 config = ConfigModel(**dict(self.config))
172
173 if config.mongodb_uri and not self.mongodb_client.is_missing_data_in_unit():
174 raise Exception("Mongodb data cannot be provided via config and relation")
175
176 # Check relations
177 self._check_missing_dependencies(config)
178
179 # Create Builder for the PodSpec
180 pod_spec_builder = PodSpecV3Builder()
181
182 # Build Container
183 container_builder = ContainerV3Builder(
184 self.app.name, image_info, config.image_pull_policy
185 )
186 container_builder.add_port(name=self.app.name, port=PORT)
187 container_builder.add_envs(
188 {
189 # General configuration
190 "ALLOW_ANONYMOUS_LOGIN": "yes",
191 "OSMLCM_GLOBAL_LOGLEVEL": config.log_level,
192 # RO configuration
193 "OSMLCM_RO_HOST": self.ro_client.host,
194 "OSMLCM_RO_PORT": self.ro_client.port,
195 "OSMLCM_RO_TENANT": "osm",
196 # Kafka configuration
197 "OSMLCM_MESSAGE_DRIVER": "kafka",
198 "OSMLCM_MESSAGE_HOST": self.kafka_client.host,
199 "OSMLCM_MESSAGE_PORT": self.kafka_client.port,
200 # Database configuration
201 "OSMLCM_DATABASE_DRIVER": "mongo",
202 "OSMLCM_DATABASE_URI": config.mongodb_uri
203 or self.mongodb_client.connection_string,
204 "OSMLCM_DATABASE_COMMONKEY": config.database_commonkey,
205 # Storage configuration
206 "OSMLCM_STORAGE_DRIVER": "mongo",
207 "OSMLCM_STORAGE_PATH": "/app/storage",
208 "OSMLCM_STORAGE_COLLECTION": "files",
209 "OSMLCM_STORAGE_URI": config.mongodb_uri
210 or self.mongodb_client.connection_string,
211 "OSMLCM_VCA_STABLEREPOURL": config.vca_stablerepourl,
212 "OSMLCM_VCA_HELM_CA_CERTS": config.vca_helm_ca_certs,
213 }
214 )
215 if config.vca_host:
216 container_builder.add_envs(
217 {
218 # VCA configuration
219 "OSMLCM_VCA_HOST": config.vca_host,
220 "OSMLCM_VCA_PORT": config.vca_port,
221 "OSMLCM_VCA_USER": config.vca_user,
222 "OSMLCM_VCA_PUBKEY": config.vca_pubkey,
223 "OSMLCM_VCA_SECRET": config.vca_secret,
224 "OSMLCM_VCA_CACERT": config.vca_cacert,
225 "OSMLCM_VCA_CLOUD": config.vca_cloud,
226 "OSMLCM_VCA_K8S_CLOUD": config.vca_k8s_cloud,
227 }
228 )
229 if config.vca_apiproxy:
230 container_builder.add_env("OSMLCM_VCA_APIPROXY", config.vca_apiproxy)
231
232 model_config_envs = {
233 f"OSMLCM_{k.upper()}": v
234 for k, v in self.config.items()
235 if k.startswith("vca_model_config")
236 }
237 if model_config_envs:
238 container_builder.add_envs(model_config_envs)
239 container = container_builder.build()
240
241 # Add container to pod spec
242 pod_spec_builder.add_container(container)
243
244 return pod_spec_builder.build()
245
246
247 if __name__ == "__main__":
248 main(LcmCharm)
249
250
251 # class ConfigurePodEvent(EventBase):
252 # """Configure Pod event"""
253
254 # pass
255
256
257 # class LcmEvents(CharmEvents):
258 # """LCM Events"""
259
260 # configure_pod = EventSource(ConfigurePodEvent)
261
262
263 # class LcmCharm(CharmBase):
264 # """LCM Charm."""
265
266 # state = StoredState()
267 # on = LcmEvents()
268
269 # def __init__(self, *args) -> NoReturn:
270 # """LCM Charm constructor."""
271 # super().__init__(*args)
272
273 # # Internal state initialization
274 # self.state.set_default(pod_spec=None)
275
276 # # Message bus data initialization
277 # self.state.set_default(message_host=None)
278 # self.state.set_default(message_port=None)
279
280 # # Database data initialization
281 # self.state.set_default(database_uri=None)
282
283 # # RO data initialization
284 # self.state.set_default(ro_host=None)
285 # self.state.set_default(ro_port=None)
286
287 # self.port = LCM_PORT
288 # self.image = OCIImageResource(self, "image")
289
290 # # Registering regular events
291 # self.framework.observe(self.on.start, self.configure_pod)
292 # self.framework.observe(self.on.config_changed, self.configure_pod)
293 # self.framework.observe(self.on.upgrade_charm, self.configure_pod)
294
295 # # Registering custom internal events
296 # self.framework.observe(self.on.configure_pod, self.configure_pod)
297
298 # # Registering required relation events
299 # self.framework.observe(
300 # self.on.kafka_relation_changed, self._on_kafka_relation_changed
301 # )
302 # self.framework.observe(
303 # self.on.mongodb_relation_changed, self._on_mongodb_relation_changed
304 # )
305 # self.framework.observe(
306 # self.on.ro_relation_changed, self._on_ro_relation_changed
307 # )
308
309 # # Registering required relation broken events
310 # self.framework.observe(
311 # self.on.kafka_relation_broken, self._on_kafka_relation_broken
312 # )
313 # self.framework.observe(
314 # self.on.mongodb_relation_broken, self._on_mongodb_relation_broken
315 # )
316 # self.framework.observe(
317 # self.on.ro_relation_broken, self._on_ro_relation_broken
318 # )
319
320 # def _on_kafka_relation_changed(self, event: EventBase) -> NoReturn:
321 # """Reads information about the kafka relation.
322
323 # Args:
324 # event (EventBase): Kafka relation event.
325 # """
326 # message_host = event.relation.data[event.unit].get("host")
327 # message_port = event.relation.data[event.unit].get("port")
328
329 # if (
330 # message_host
331 # and message_port
332 # and (
333 # self.state.message_host != message_host
334 # or self.state.message_port != message_port
335 # )
336 # ):
337 # self.state.message_host = message_host
338 # self.state.message_port = message_port
339 # self.on.configure_pod.emit()
340
341 # def _on_kafka_relation_broken(self, event: EventBase) -> NoReturn:
342 # """Clears data from kafka relation.
343
344 # Args:
345 # event (EventBase): Kafka relation event.
346 # """
347 # self.state.message_host = None
348 # self.state.message_port = None
349 # self.on.configure_pod.emit()
350
351 # def _on_mongodb_relation_changed(self, event: EventBase) -> NoReturn:
352 # """Reads information about the DB relation.
353
354 # Args:
355 # event (EventBase): DB relation event.
356 # """
357 # database_uri = event.relation.data[event.unit].get("connection_string")
358
359 # if database_uri and self.state.database_uri != database_uri:
360 # self.state.database_uri = database_uri
361 # self.on.configure_pod.emit()
362
363 # def _on_mongodb_relation_broken(self, event: EventBase) -> NoReturn:
364 # """Clears data from mongodb relation.
365
366 # Args:
367 # event (EventBase): DB relation event.
368 # """
369 # self.state.database_uri = None
370 # self.on.configure_pod.emit()
371
372 # def _on_ro_relation_changed(self, event: EventBase) -> NoReturn:
373 # """Reads information about the RO relation.
374
375 # Args:
376 # event (EventBase): Keystone relation event.
377 # """
378 # ro_host = event.relation.data[event.unit].get("host")
379 # ro_port = event.relation.data[event.unit].get("port")
380
381 # if (
382 # ro_host
383 # and ro_port
384 # and (self.state.ro_host != ro_host or self.state.ro_port != ro_port)
385 # ):
386 # self.state.ro_host = ro_host
387 # self.state.ro_port = ro_port
388 # self.on.configure_pod.emit()
389
390 # def _on_ro_relation_broken(self, event: EventBase) -> NoReturn:
391 # """Clears data from ro relation.
392
393 # Args:
394 # event (EventBase): Keystone relation event.
395 # """
396 # self.state.ro_host = None
397 # self.state.ro_port = None
398 # self.on.configure_pod.emit()
399
400 # def _missing_relations(self) -> str:
401 # """Checks if there missing relations.
402
403 # Returns:
404 # str: string with missing relations
405 # """
406 # data_status = {
407 # "kafka": self.state.message_host,
408 # "mongodb": self.state.database_uri,
409 # "ro": self.state.ro_host,
410 # }
411
412 # missing_relations = [k for k, v in data_status.items() if not v]
413
414 # return ", ".join(missing_relations)
415
416 # @property
417 # def relation_state(self) -> Dict[str, Any]:
418 # """Collects relation state configuration for pod spec assembly.
419
420 # Returns:
421 # Dict[str, Any]: relation state information.
422 # """
423 # relation_state = {
424 # "message_host": self.state.message_host,
425 # "message_port": self.state.message_port,
426 # "database_uri": self.state.database_uri,
427 # "ro_host": self.state.ro_host,
428 # "ro_port": self.state.ro_port,
429 # }
430
431 # return relation_state
432
433 # def configure_pod(self, event: EventBase) -> NoReturn:
434 # """Assemble the pod spec and apply it, if possible.
435
436 # Args:
437 # event (EventBase): Hook or Relation event that started the
438 # function.
439 # """
440 # if missing := self._missing_relations():
441 # self.unit.status = BlockedStatus(
442 # "Waiting for {0} relation{1}".format(
443 # missing, "s" if "," in missing else ""
444 # )
445 # )
446 # return
447
448 # if not self.unit.is_leader():
449 # self.unit.status = ActiveStatus("ready")
450 # return
451
452 # self.unit.status = MaintenanceStatus("Assembling pod spec")
453
454 # # Fetch image information
455 # try:
456 # self.unit.status = MaintenanceStatus("Fetching image information")
457 # image_info = self.image.fetch()
458 # except OCIImageResourceError:
459 # self.unit.status = BlockedStatus("Error fetching image information")
460 # return
461
462 # try:
463 # pod_spec = make_pod_spec(
464 # image_info,
465 # self.model.config,
466 # self.relation_state,
467 # self.model.app.name,
468 # self.port,
469 # )
470 # except ValueError as exc:
471 # logger.exception("Config/Relation data validation error")
472 # self.unit.status = BlockedStatus(str(exc))
473 # return
474
475 # if self.state.pod_spec != pod_spec:
476 # self.model.pod.set_spec(pod_spec)
477 # self.state.pod_spec = pod_spec
478
479 # self.unit.status = ActiveStatus("ready")
480
481
482 # if __name__ == "__main__":
483 # main(LcmCharm)