2 # Copyright 2021 Canonical Ltd.
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
8 # http://www.apache.org/licenses/LICENSE-2.0
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
16 # For those usages not covered by the Apache License, Version 2.0 please
17 # contact: legal@canonical.com
19 # To get in touch with the maintainers, please contact:
20 # osm-charmers@lists.launchpad.net
23 # pylint: disable=E0213
26 from ipaddress
import ip_network
28 from typing
import NoReturn
, Optional
29 from urllib
.parse
import urlparse
32 from oci_image
import OCIImageResource
33 from ops
.framework
import EventBase
34 from ops
.main
import main
35 from opslib
.osm
.charm
import CharmedOsmBase
36 from opslib
.osm
.interfaces
.prometheus
import PrometheusServer
37 from opslib
.osm
.pod
import (
40 IngressResourceV3Builder
,
43 from opslib
.osm
.validator
import (
50 logger
= logging
.getLogger(__name__
)
55 class ConfigModel(ModelValidator
):
59 site_url
: Optional
[str]
60 cluster_issuer
: Optional
[str]
61 ingress_class
: Optional
[str]
62 ingress_whitelist_source_range
: Optional
[str]
63 tls_secret_name
: Optional
[str]
64 enable_web_admin_api
: bool
65 image_pull_policy
: str
66 security_context
: bool
67 web_config_username
: str
68 web_config_password
: str
70 @validator("web_subpath")
71 def validate_web_subpath(cls
, v
):
73 raise ValueError("web-subpath must be a non-empty string")
76 @validator("max_file_size")
77 def validate_max_file_size(cls
, v
):
79 raise ValueError("value must be equal or greater than 0")
82 @validator("site_url")
83 def validate_site_url(cls
, v
):
86 if not parsed
.scheme
.startswith("http"):
87 raise ValueError("value must start with http")
90 @validator("ingress_whitelist_source_range")
91 def validate_ingress_whitelist_source_range(cls
, v
):
96 @validator("image_pull_policy")
97 def validate_image_pull_policy(cls
, v
):
100 "ifnotpresent": "IfNotPresent",
104 if v
not in values
.keys():
105 raise ValueError("value must be always, ifnotpresent or never")
109 class PrometheusCharm(CharmedOsmBase
):
111 """Prometheus Charm."""
113 def __init__(self
, *args
) -> NoReturn
:
114 """Prometheus Charm constructor."""
115 super().__init
__(*args
, oci_image
="image")
117 # Registering provided relation events
118 self
.prometheus
= PrometheusServer(self
, "prometheus")
119 self
.framework
.observe(
120 self
.on
.prometheus_relation_joined
, # pylint: disable=E1101
121 self
._publish
_prometheus
_info
,
124 # Registering actions
125 self
.framework
.observe(
126 self
.on
.backup_action
, # pylint: disable=E1101
127 self
._on
_backup
_action
,
130 def _publish_prometheus_info(self
, event
: EventBase
) -> NoReturn
:
131 config
= ConfigModel(**dict(self
.config
))
132 self
.prometheus
.publish_info(
135 user
=config
.web_config_username
,
136 password
=config
.web_config_password
,
139 def _on_backup_action(self
, event
: EventBase
) -> NoReturn
:
140 url
= f
"http://{self.model.app.name}:{PORT}/api/v1/admin/tsdb/snapshot"
141 result
= requests
.post(url
)
143 if result
.status_code
== 200:
144 event
.set_results({"backup-name": result
.json()["name"]})
146 event
.fail(f
"status-code: {result.status_code}")
148 def _build_config_file(self
, config
: ConfigModel
):
149 files_builder
= FilesV3Builder()
150 files_builder
.add_file(
154 " scrape_interval: 15s\n"
155 " evaluation_interval: 15s\n"
158 " - static_configs:\n"
162 " - job_name: 'prometheus'\n"
164 f
" - targets: [{config.default_target}]\n"
167 return files_builder
.build()
169 def _build_webconfig_file(self
):
170 files_builder
= FilesV3Builder()
171 files_builder
.add_file("web.yml", "web-config-file", secret
=True)
172 return files_builder
.build()
174 def build_pod_spec(self
, image_info
):
176 config
= ConfigModel(**dict(self
.config
))
177 # Create Builder for the PodSpec
178 pod_spec_builder
= PodSpecV3Builder(
179 enable_security_context
=config
.security_context
182 # Build Backup Container
183 backup_image
= OCIImageResource(self
, "backup-image")
184 backup_image_info
= backup_image
.fetch()
185 backup_container_builder
= ContainerV3Builder("prom-backup", backup_image_info
)
186 backup_container
= backup_container_builder
.build()
188 # Add backup container to pod spec
189 pod_spec_builder
.add_container(backup_container
)
192 prometheus_secret_name
= f
"{self.app.name}-secret"
193 pod_spec_builder
.add_secret(
194 prometheus_secret_name
,
197 "basic_auth_users:\n"
198 f
" {config.web_config_username}: {self._hash_password(config.web_config_password)}\n"
204 container_builder
= ContainerV3Builder(
207 config
.image_pull_policy
,
208 run_as_non_root
=config
.security_context
,
210 container_builder
.add_port(name
=self
.app
.name
, port
=PORT
)
211 token
= self
._base
64_encode
(
212 f
"{config.web_config_username}:{config.web_config_password}"
214 container_builder
.add_http_readiness_probe(
217 initial_delay_seconds
=10,
219 http_headers
=[("Authorization", f
"Basic {token}")],
221 container_builder
.add_http_liveness_probe(
224 initial_delay_seconds
=30,
226 http_headers
=[("Authorization", f
"Basic {token}")],
230 "--config.file=/etc/prometheus/prometheus.yml",
231 "--web.config.file=/etc/prometheus/web-config/web.yml",
232 "--storage.tsdb.path=/prometheus",
233 "--web.console.libraries=/usr/share/prometheus/console_libraries",
234 "--web.console.templates=/usr/share/prometheus/consoles",
235 f
"--web.route-prefix={config.web_subpath}",
236 f
"--web.external-url=http://localhost:{PORT}{config.web_subpath}",
238 if config
.enable_web_admin_api
:
239 command
.append("--web.enable-admin-api")
240 container_builder
.add_command(command
)
241 container_builder
.add_volume_config(
242 "config", "/etc/prometheus", self
._build
_config
_file
(config
)
244 container_builder
.add_volume_config(
246 "/etc/prometheus/web-config",
247 self
._build
_webconfig
_file
(),
248 secret_name
=prometheus_secret_name
,
250 container
= container_builder
.build()
251 # Add container to pod spec
252 pod_spec_builder
.add_container(container
)
253 # Add ingress resources to pod spec if site url exists
255 parsed
= urlparse(config
.site_url
)
257 "nginx.ingress.kubernetes.io/proxy-body-size": "{}".format(
258 str(config
.max_file_size
) + "m"
259 if config
.max_file_size
> 0
260 else config
.max_file_size
263 if config
.ingress_class
:
264 annotations
["kubernetes.io/ingress.class"] = config
.ingress_class
265 ingress_resource_builder
= IngressResourceV3Builder(
266 f
"{self.app.name}-ingress", annotations
269 if config
.ingress_whitelist_source_range
:
271 "nginx.ingress.kubernetes.io/whitelist-source-range"
272 ] = config
.ingress_whitelist_source_range
274 if config
.cluster_issuer
:
275 annotations
["cert-manager.io/cluster-issuer"] = config
.cluster_issuer
277 if parsed
.scheme
== "https":
278 ingress_resource_builder
.add_tls(
279 [parsed
.hostname
], config
.tls_secret_name
282 annotations
["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
284 ingress_resource_builder
.add_rule(parsed
.hostname
, self
.app
.name
, PORT
)
285 ingress_resource
= ingress_resource_builder
.build()
286 pod_spec_builder
.add_ingress_resource(ingress_resource
)
287 return pod_spec_builder
.build()
289 def _hash_password(self
, password
):
290 hashed_password
= bcrypt
.hashpw(password
.encode("utf-8"), bcrypt
.gensalt())
291 return hashed_password
.decode()
293 def _base64_encode(self
, phrase
: str) -> str:
294 return base64
.b64encode(phrase
.encode("utf-8")).decode("utf-8")
297 if __name__
== "__main__":
298 main(PrometheusCharm
)