blob: 5cd163da46860a2129878ed3d359d9f4acfac1d4 [file] [log] [blame]
sousaedu2459af62021-01-15 16:50:26 +00001#!/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
David Garcia49379ce2021-02-24 13:48:22 +010023# pylint: disable=E0213
24
David Garcia49379ce2021-02-24 13:48:22 +010025from ipaddress import ip_network
David Garciac753dc52021-03-17 15:28:47 +010026import logging
27from typing import NoReturn, Optional
28from urllib.parse import urlparse
sousaedu2459af62021-01-15 16:50:26 +000029
sousaedu1ec36cc2021-03-03 01:12:49 +010030from oci_image import OCIImageResource
David Garcia49379ce2021-02-24 13:48:22 +010031from ops.framework import EventBase
sousaedu2459af62021-01-15 16:50:26 +000032from ops.main import main
David Garcia49379ce2021-02-24 13:48:22 +010033from opslib.osm.charm import CharmedOsmBase
David Garciac753dc52021-03-17 15:28:47 +010034from opslib.osm.interfaces.prometheus import PrometheusServer
David Garcia49379ce2021-02-24 13:48:22 +010035from opslib.osm.pod import (
David Garcia49379ce2021-02-24 13:48:22 +010036 ContainerV3Builder,
David Garciac753dc52021-03-17 15:28:47 +010037 FilesV3Builder,
38 IngressResourceV3Builder,
David Garcia49379ce2021-02-24 13:48:22 +010039 PodSpecV3Builder,
40)
David Garcia49379ce2021-02-24 13:48:22 +010041from opslib.osm.validator import (
42 ModelValidator,
43 validator,
44)
sousaedu1ec36cc2021-03-03 01:12:49 +010045import requests
David Garcia49379ce2021-02-24 13:48:22 +010046
sousaedu2459af62021-01-15 16:50:26 +000047
48logger = logging.getLogger(__name__)
49
David Garcia49379ce2021-02-24 13:48:22 +010050PORT = 9090
sousaedu2459af62021-01-15 16:50:26 +000051
52
David Garcia49379ce2021-02-24 13:48:22 +010053class 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
sousaedu2459af62021-01-15 16:50:26 +000087
88
David Garcia49379ce2021-02-24 13:48:22 +010089class PrometheusCharm(CharmedOsmBase):
sousaedu2459af62021-01-15 16:50:26 +000090
sousaedu2459af62021-01-15 16:50:26 +000091 """Prometheus Charm."""
92
sousaedu2459af62021-01-15 16:50:26 +000093 def __init__(self, *args) -> NoReturn:
94 """Prometheus Charm constructor."""
David Garcia49379ce2021-02-24 13:48:22 +010095 super().__init__(*args, oci_image="image")
sousaedu2459af62021-01-15 16:50:26 +000096
97 # Registering provided relation events
David Garcia49379ce2021-02-24 13:48:22 +010098 self.prometheus = PrometheusServer(self, "prometheus")
sousaedu2459af62021-01-15 16:50:26 +000099 self.framework.observe(
David Garcia49379ce2021-02-24 13:48:22 +0100100 self.on.prometheus_relation_joined, # pylint: disable=E1101
101 self._publish_prometheus_info,
sousaedu2459af62021-01-15 16:50:26 +0000102 )
103
sousaedu1ec36cc2021-03-03 01:12:49 +0100104 # Registering actions
105 self.framework.observe(
106 self.on.backup_action, # pylint: disable=E1101
107 self._on_backup_action,
108 )
109
sousaedu2459af62021-01-15 16:50:26 +0000110 def _publish_prometheus_info(self, event: EventBase) -> NoReturn:
David Garcia49379ce2021-02-24 13:48:22 +0100111 self.prometheus.publish_info(self.app.name, PORT)
sousaedu2459af62021-01-15 16:50:26 +0000112
sousaedu1ec36cc2021-03-03 01:12:49 +0100113 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
David Garcia49379ce2021-02-24 13:48:22 +0100122 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()
sousaedu1ec36cc2021-03-03 01:12:49 +0100148
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
David Garcia49379ce2021-02-24 13:48:22 +0100157 # 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 ),
sousaedu2459af62021-01-15 16:50:26 +0000199 }
David Garcia49379ce2021-02-24 13:48:22 +0100200 ingress_resource_builder = IngressResourceV3Builder(
201 f"{self.app.name}-ingress", annotations
sousaedu2459af62021-01-15 16:50:26 +0000202 )
sousaedu2459af62021-01-15 16:50:26 +0000203
David Garcia49379ce2021-02-24 13:48:22 +0100204 if config.ingress_whitelist_source_range:
205 annotations[
206 "nginx.ingress.kubernetes.io/whitelist-source-range"
207 ] = config.ingress_whitelist_source_range
sousaedu2459af62021-01-15 16:50:26 +0000208
David Garcia49379ce2021-02-24 13:48:22 +0100209 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()
sousaedu2459af62021-01-15 16:50:26 +0000220
221
222if __name__ == "__main__":
223 main(PrometheusCharm)