47533ddf6e346409beb340c2dc225e29d393c42b
[osm/devops.git] / installers / charm / prometheus / 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 from ipaddress import ip_network
26 import logging
27 from typing import NoReturn, Optional
28 from urllib.parse import urlparse
29
30 from ops.framework import EventBase
31 from ops.main import main
32 from opslib.osm.charm import CharmedOsmBase
33 from opslib.osm.interfaces.prometheus import PrometheusServer
34 from opslib.osm.pod import (
35 ContainerV3Builder,
36 FilesV3Builder,
37 IngressResourceV3Builder,
38 PodSpecV3Builder,
39 )
40 from opslib.osm.validator import (
41 ModelValidator,
42 validator,
43 )
44
45
46 logger = logging.getLogger(__name__)
47
48 PORT = 9090
49
50
51 class ConfigModel(ModelValidator):
52 web_subpath: str
53 default_target: str
54 max_file_size: int
55 site_url: Optional[str]
56 ingress_whitelist_source_range: Optional[str]
57 tls_secret_name: Optional[str]
58 enable_web_admin_api: bool
59
60 @validator("web_subpath")
61 def validate_web_subpath(cls, v):
62 if len(v) < 1:
63 raise ValueError("web-subpath must be a non-empty string")
64 return v
65
66 @validator("max_file_size")
67 def validate_max_file_size(cls, v):
68 if v < 0:
69 raise ValueError("value must be equal or greater than 0")
70 return v
71
72 @validator("site_url")
73 def validate_site_url(cls, v):
74 if v:
75 parsed = urlparse(v)
76 if not parsed.scheme.startswith("http"):
77 raise ValueError("value must start with http")
78 return v
79
80 @validator("ingress_whitelist_source_range")
81 def validate_ingress_whitelist_source_range(cls, v):
82 if v:
83 ip_network(v)
84 return v
85
86
87 class PrometheusCharm(CharmedOsmBase):
88
89 """Prometheus Charm."""
90
91 def __init__(self, *args) -> NoReturn:
92 """Prometheus Charm constructor."""
93 super().__init__(*args, oci_image="image")
94
95 # Registering provided relation events
96 self.prometheus = PrometheusServer(self, "prometheus")
97 self.framework.observe(
98 self.on.prometheus_relation_joined, # pylint: disable=E1101
99 self._publish_prometheus_info,
100 )
101
102 def _publish_prometheus_info(self, event: EventBase) -> NoReturn:
103 self.prometheus.publish_info(self.app.name, PORT)
104
105 def _build_files(self, config: ConfigModel):
106 files_builder = FilesV3Builder()
107 files_builder.add_file(
108 "prometheus.yml",
109 (
110 "global:\n"
111 " scrape_interval: 15s\n"
112 " evaluation_interval: 15s\n"
113 "alerting:\n"
114 " alertmanagers:\n"
115 " - static_configs:\n"
116 " - targets:\n"
117 "rule_files:\n"
118 "scrape_configs:\n"
119 " - job_name: 'prometheus'\n"
120 " static_configs:\n"
121 f" - targets: [{config.default_target}]\n"
122 ),
123 )
124 return files_builder.build()
125
126 def build_pod_spec(self, image_info):
127 # Validate config
128 config = ConfigModel(**dict(self.config))
129 # Create Builder for the PodSpec
130 pod_spec_builder = PodSpecV3Builder()
131 # Build Container
132 container_builder = ContainerV3Builder(self.app.name, image_info)
133 container_builder.add_port(name=self.app.name, port=PORT)
134 container_builder.add_http_readiness_probe(
135 "/-/ready",
136 PORT,
137 initial_delay_seconds=10,
138 timeout_seconds=30,
139 )
140 container_builder.add_http_liveness_probe(
141 "/-/healthy",
142 PORT,
143 initial_delay_seconds=30,
144 period_seconds=30,
145 )
146 command = [
147 "/bin/prometheus",
148 "--config.file=/etc/prometheus/prometheus.yml",
149 "--storage.tsdb.path=/prometheus",
150 "--web.console.libraries=/usr/share/prometheus/console_libraries",
151 "--web.console.templates=/usr/share/prometheus/consoles",
152 f"--web.route-prefix={config.web_subpath}",
153 f"--web.external-url=http://localhost:{PORT}{config.web_subpath}",
154 ]
155 if config.enable_web_admin_api:
156 command.append("--web.enable-admin-api")
157 container_builder.add_command(command)
158 container_builder.add_volume_config(
159 "config", "/etc/prometheus", self._build_files(config)
160 )
161 container = container_builder.build()
162 # Add container to pod spec
163 pod_spec_builder.add_container(container)
164 # Add ingress resources to pod spec if site url exists
165 if config.site_url:
166 parsed = urlparse(config.site_url)
167 annotations = {
168 "nginx.ingress.kubernetes.io/proxy-body-size": "{}".format(
169 str(config.max_file_size) + "m"
170 if config.max_file_size > 0
171 else config.max_file_size
172 ),
173 }
174 ingress_resource_builder = IngressResourceV3Builder(
175 f"{self.app.name}-ingress", annotations
176 )
177
178 if config.ingress_whitelist_source_range:
179 annotations[
180 "nginx.ingress.kubernetes.io/whitelist-source-range"
181 ] = config.ingress_whitelist_source_range
182
183 if parsed.scheme == "https":
184 ingress_resource_builder.add_tls(
185 [parsed.hostname], config.tls_secret_name
186 )
187 else:
188 annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
189
190 ingress_resource_builder.add_rule(parsed.hostname, self.app.name, PORT)
191 ingress_resource = ingress_resource_builder.build()
192 pod_spec_builder.add_ingress_resource(ingress_resource)
193 return pod_spec_builder.build()
194
195
196 if __name__ == "__main__":
197 main(PrometheusCharm)