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