CharmHub and new kafka and zookeeper charms
[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
91 on = KafkaEvents()
92
93 def __init__(self, *args) -> NoReturn:
94 super().__init__(
95 *args,
96 oci_image="image",
97 vscode_workspace=VSCODE_WORKSPACE,
98 )
99 if self.config.get("debug_mode"):
100 self.enable_debug_mode(
101 pubkey=self.config.get("debug_pubkey"),
102 hostpaths={
103 "POL": {
104 "hostpath": self.config.get("debug_pol_local_path"),
105 "container-path": "/usr/lib/python3/dist-packages/osm_policy_module",
106 },
107 "osm_common": {
108 "hostpath": self.config.get("debug_common_local_path"),
109 "container-path": "/usr/lib/python3/dist-packages/osm_common",
110 },
111 },
112 )
113 self.kafka = KafkaRequires(self)
114 self.framework.observe(self.on.kafka_available, self.configure_pod)
115 self.framework.observe(self.on.kafka_broken, self.configure_pod)
116
117 self.mongodb_client = MongoClient(self, "mongodb")
118 self.framework.observe(self.on["mongodb"].relation_changed, self.configure_pod)
119 self.framework.observe(self.on["mongodb"].relation_broken, self.configure_pod)
120
121 self.mysql_client = MysqlClient(self, "mysql")
122 self.framework.observe(self.on["mysql"].relation_changed, self.configure_pod)
123 self.framework.observe(self.on["mysql"].relation_broken, self.configure_pod)
124
125 def _check_missing_dependencies(self, config: ConfigModel):
126 missing_relations = []
127
128 if not self.kafka.host or not self.kafka.port:
129 missing_relations.append("kafka")
130 if not config.mongodb_uri and self.mongodb_client.is_missing_data_in_unit():
131 missing_relations.append("mongodb")
132 if not config.mysql_uri and self.mysql_client.is_missing_data_in_unit():
133 missing_relations.append("mysql")
134 if missing_relations:
135 raise RelationsMissing(missing_relations)
136
137 def build_pod_spec(self, image_info):
138 # Validate config
139 config = ConfigModel(**dict(self.config))
140
141 if config.mongodb_uri and not self.mongodb_client.is_missing_data_in_unit():
142 raise Exception("Mongodb data cannot be provided via config and relation")
143 if config.mysql_uri and not self.mysql_client.is_missing_data_in_unit():
144 raise Exception("Mysql data cannot be provided via config and relation")
145
146 # Check relations
147 self._check_missing_dependencies(config)
148
149 security_context_enabled = (
150 config.security_context if not config.debug_mode else False
151 )
152
153 # Create Builder for the PodSpec
154 pod_spec_builder = PodSpecV3Builder(
155 enable_security_context=security_context_enabled
156 )
157
158 # Add secrets to the pod
159 mongodb_secret_name = f"{self.app.name}-mongodb-secret"
160 pod_spec_builder.add_secret(
161 mongodb_secret_name,
162 {"uri": config.mongodb_uri or self.mongodb_client.connection_string},
163 )
164 mysql_secret_name = f"{self.app.name}-mysql-secret"
165 pod_spec_builder.add_secret(
166 mysql_secret_name,
167 {
168 "uri": config.mysql_uri
169 or self.mysql_client.get_root_uri(DEFAULT_MYSQL_DATABASE)
170 },
171 )
172
173 # Build Container
174 container_builder = ContainerV3Builder(
175 self.app.name,
176 image_info,
177 config.image_pull_policy,
178 run_as_non_root=security_context_enabled,
179 )
180 container_builder.add_port(name=self.app.name, port=PORT)
181 container_builder.add_envs(
182 {
183 # General configuration
184 "ALLOW_ANONYMOUS_LOGIN": "yes",
185 "OSMPOL_GLOBAL_LOGLEVEL": config.log_level,
186 # Kafka configuration
187 "OSMPOL_MESSAGE_DRIVER": "kafka",
188 "OSMPOL_MESSAGE_HOST": self.kafka.host,
189 "OSMPOL_MESSAGE_PORT": self.kafka.port,
190 # Database configuration
191 "OSMPOL_DATABASE_DRIVER": "mongo",
192 }
193 )
194 container_builder.add_secret_envs(
195 mongodb_secret_name, {"OSMPOL_DATABASE_URI": "uri"}
196 )
197 container_builder.add_secret_envs(
198 mysql_secret_name, {"OSMPOL_SQL_DATABASE_URI": "uri"}
199 )
200 container = container_builder.build()
201
202 # Add Pod restart policy
203 restart_policy = PodRestartPolicy()
204 restart_policy.add_secrets(
205 secret_names=(mongodb_secret_name, mysql_secret_name)
206 )
207 pod_spec_builder.set_restart_policy(restart_policy)
208
209 # Add container to pod spec
210 pod_spec_builder.add_container(container)
211
212 return pod_spec_builder.build()
213
214
215 VSCODE_WORKSPACE = {
216 "folders": [
217 {"path": "/usr/lib/python3/dist-packages/osm_policy_module"},
218 {"path": "/usr/lib/python3/dist-packages/osm_common"},
219 ],
220 "settings": {},
221 "launch": {
222 "version": "0.2.0",
223 "configurations": [
224 {
225 "name": "POL",
226 "type": "python",
227 "request": "launch",
228 "module": "osm_policy_module.cmd.policy_module_agent",
229 "justMyCode": False,
230 }
231 ],
232 },
233 }
234
235
236 if __name__ == "__main__":
237 main(PolCharm)