Reformat files according to new black validation
[osm/devops.git] / installers / charm / pol / 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 import re
28 from typing import NoReturn, Optional
29
30 from charms.kafka_k8s.v0.kafka import KafkaEvents, KafkaRequires
31 from ops.main import main
32 from opslib.osm.charm import CharmedOsmBase, RelationsMissing
33 from opslib.osm.interfaces.mongo import MongoClient
34 from opslib.osm.interfaces.mysql import MysqlClient
35 from opslib.osm.pod import (
36 ContainerV3Builder,
37 PodRestartPolicy,
38 PodSpecV3Builder,
39 )
40 from opslib.osm.validator import ModelValidator, validator
41
42
43 logger = logging.getLogger(__name__)
44
45 PORT = 9999
46 DEFAULT_MYSQL_DATABASE = "pol"
47
48
49 class ConfigModel(ModelValidator):
50 log_level: str
51 mongodb_uri: Optional[str]
52 mysql_uri: Optional[str]
53 image_pull_policy: str
54 debug_mode: bool
55 security_context: bool
56
57 @validator("log_level")
58 def validate_log_level(cls, v):
59 if v not in {"INFO", "DEBUG"}:
60 raise ValueError("value must be INFO or DEBUG")
61 return v
62
63 @validator("mongoddb_uri")
64 def validate_mongodb_uri(cls, v):
65 if v and not v.startswith("mongodb://"):
66 raise ValueError("mongodb_uri is not properly formed")
67 return v
68
69 @validator("mysql_uri")
70 def validate_mysql_uri(cls, v):
71 pattern = re.compile("^mysql:\/\/.*:.*@.*:\d+\/.*$") # noqa: W605
72 if v and not pattern.search(v):
73 raise ValueError("mysql_uri is not properly formed")
74 return v
75
76 @validator("image_pull_policy")
77 def validate_image_pull_policy(cls, v):
78 values = {
79 "always": "Always",
80 "ifnotpresent": "IfNotPresent",
81 "never": "Never",
82 }
83 v = v.lower()
84 if v not in values.keys():
85 raise ValueError("value must be always, ifnotpresent or never")
86 return values[v]
87
88
89 class PolCharm(CharmedOsmBase):
90 on = KafkaEvents()
91
92 def __init__(self, *args) -> NoReturn:
93 super().__init__(
94 *args,
95 oci_image="image",
96 vscode_workspace=VSCODE_WORKSPACE,
97 )
98 if self.config.get("debug_mode"):
99 self.enable_debug_mode(
100 pubkey=self.config.get("debug_pubkey"),
101 hostpaths={
102 "POL": {
103 "hostpath": self.config.get("debug_pol_local_path"),
104 "container-path": "/usr/lib/python3/dist-packages/osm_policy_module",
105 },
106 "osm_common": {
107 "hostpath": self.config.get("debug_common_local_path"),
108 "container-path": "/usr/lib/python3/dist-packages/osm_common",
109 },
110 },
111 )
112 self.kafka = KafkaRequires(self)
113 self.framework.observe(self.on.kafka_available, self.configure_pod)
114 self.framework.observe(self.on.kafka_broken, self.configure_pod)
115
116 self.mongodb_client = MongoClient(self, "mongodb")
117 self.framework.observe(self.on["mongodb"].relation_changed, self.configure_pod)
118 self.framework.observe(self.on["mongodb"].relation_broken, self.configure_pod)
119
120 self.mysql_client = MysqlClient(self, "mysql")
121 self.framework.observe(self.on["mysql"].relation_changed, self.configure_pod)
122 self.framework.observe(self.on["mysql"].relation_broken, self.configure_pod)
123
124 def _check_missing_dependencies(self, config: ConfigModel):
125 missing_relations = []
126
127 if not self.kafka.host or not self.kafka.port:
128 missing_relations.append("kafka")
129 if not config.mongodb_uri and self.mongodb_client.is_missing_data_in_unit():
130 missing_relations.append("mongodb")
131 if not config.mysql_uri and self.mysql_client.is_missing_data_in_unit():
132 missing_relations.append("mysql")
133 if missing_relations:
134 raise RelationsMissing(missing_relations)
135
136 def build_pod_spec(self, image_info):
137 # Validate config
138 config = ConfigModel(**dict(self.config))
139
140 if config.mongodb_uri and not self.mongodb_client.is_missing_data_in_unit():
141 raise Exception("Mongodb data cannot be provided via config and relation")
142 if config.mysql_uri and not self.mysql_client.is_missing_data_in_unit():
143 raise Exception("Mysql data cannot be provided via config and relation")
144
145 # Check relations
146 self._check_missing_dependencies(config)
147
148 security_context_enabled = (
149 config.security_context if not config.debug_mode else False
150 )
151
152 # Create Builder for the PodSpec
153 pod_spec_builder = PodSpecV3Builder(
154 enable_security_context=security_context_enabled
155 )
156
157 # Add secrets to the pod
158 mongodb_secret_name = f"{self.app.name}-mongodb-secret"
159 pod_spec_builder.add_secret(
160 mongodb_secret_name,
161 {"uri": config.mongodb_uri or self.mongodb_client.connection_string},
162 )
163 mysql_secret_name = f"{self.app.name}-mysql-secret"
164 pod_spec_builder.add_secret(
165 mysql_secret_name,
166 {
167 "uri": config.mysql_uri
168 or self.mysql_client.get_root_uri(DEFAULT_MYSQL_DATABASE)
169 },
170 )
171
172 # Build Container
173 container_builder = ContainerV3Builder(
174 self.app.name,
175 image_info,
176 config.image_pull_policy,
177 run_as_non_root=security_context_enabled,
178 )
179 container_builder.add_port(name=self.app.name, port=PORT)
180 container_builder.add_envs(
181 {
182 # General configuration
183 "ALLOW_ANONYMOUS_LOGIN": "yes",
184 "OSMPOL_GLOBAL_LOGLEVEL": config.log_level,
185 # Kafka configuration
186 "OSMPOL_MESSAGE_DRIVER": "kafka",
187 "OSMPOL_MESSAGE_HOST": self.kafka.host,
188 "OSMPOL_MESSAGE_PORT": self.kafka.port,
189 # Database configuration
190 "OSMPOL_DATABASE_DRIVER": "mongo",
191 }
192 )
193 container_builder.add_secret_envs(
194 mongodb_secret_name, {"OSMPOL_DATABASE_URI": "uri"}
195 )
196 container_builder.add_secret_envs(
197 mysql_secret_name, {"OSMPOL_SQL_DATABASE_URI": "uri"}
198 )
199 container = container_builder.build()
200
201 # Add Pod restart policy
202 restart_policy = PodRestartPolicy()
203 restart_policy.add_secrets(
204 secret_names=(mongodb_secret_name, mysql_secret_name)
205 )
206 pod_spec_builder.set_restart_policy(restart_policy)
207
208 # Add container to pod spec
209 pod_spec_builder.add_container(container)
210
211 return pod_spec_builder.build()
212
213
214 VSCODE_WORKSPACE = {
215 "folders": [
216 {"path": "/usr/lib/python3/dist-packages/osm_policy_module"},
217 {"path": "/usr/lib/python3/dist-packages/osm_common"},
218 ],
219 "settings": {},
220 "launch": {
221 "version": "0.2.0",
222 "configurations": [
223 {
224 "name": "POL",
225 "type": "python",
226 "request": "launch",
227 "module": "osm_policy_module.cmd.policy_module_agent",
228 "justMyCode": False,
229 }
230 ],
231 },
232 }
233
234
235 if __name__ == "__main__":
236 main(PolCharm)