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
):
110 """Prometheus Charm."""
112 def __init__(self
, *args
) -> NoReturn
:
113 """Prometheus Charm constructor."""
114 super().__init
__(*args
, oci_image
="image")
116 # Registering provided relation events
117 self
.prometheus
= PrometheusServer(self
, "prometheus")
118 self
.framework
.observe(
119 self
.on
.prometheus_relation_joined
, # pylint: disable=E1101
120 self
._publish
_prometheus
_info
,
123 # Registering actions
124 self
.framework
.observe(
125 self
.on
.backup_action
, # pylint: disable=E1101
126 self
._on
_backup
_action
,
129 def _publish_prometheus_info(self
, event
: EventBase
) -> NoReturn
:
130 config
= ConfigModel(**dict(self
.config
))
131 self
.prometheus
.publish_info(
134 user
=config
.web_config_username
,
135 password
=config
.web_config_password
,
138 def _on_backup_action(self
, event
: EventBase
) -> NoReturn
:
139 url
= f
"http://{self.model.app.name}:{PORT}/api/v1/admin/tsdb/snapshot"
140 result
= requests
.post(url
)
142 if result
.status_code
== 200:
143 event
.set_results({"backup-name": result
.json()["name"]})
145 event
.fail(f
"status-code: {result.status_code}")
147 def _build_config_file(self
, config
: ConfigModel
):
148 files_builder
= FilesV3Builder()
149 files_builder
.add_file(
153 " scrape_interval: 15s\n"
154 " evaluation_interval: 15s\n"
157 " - static_configs:\n"
161 " - job_name: 'prometheus'\n"
163 f
" - targets: [{config.default_target}]\n"
166 return files_builder
.build()
168 def _build_webconfig_file(self
):
169 files_builder
= FilesV3Builder()
170 files_builder
.add_file("web.yml", "web-config-file", secret
=True)
171 return files_builder
.build()
173 def build_pod_spec(self
, image_info
):
175 config
= ConfigModel(**dict(self
.config
))
176 # Create Builder for the PodSpec
177 pod_spec_builder
= PodSpecV3Builder(
178 enable_security_context
=config
.security_context
181 # Build Backup Container
182 backup_image
= OCIImageResource(self
, "backup-image")
183 backup_image_info
= backup_image
.fetch()
184 backup_container_builder
= ContainerV3Builder("prom-backup", backup_image_info
)
185 backup_container
= backup_container_builder
.build()
187 # Add backup container to pod spec
188 pod_spec_builder
.add_container(backup_container
)
191 prometheus_secret_name
= f
"{self.app.name}-secret"
192 pod_spec_builder
.add_secret(
193 prometheus_secret_name
,
196 "basic_auth_users:\n"
197 f
" {config.web_config_username}: {self._hash_password(config.web_config_password)}\n"
203 container_builder
= ContainerV3Builder(
206 config
.image_pull_policy
,
207 run_as_non_root
=config
.security_context
,
209 container_builder
.add_port(name
=self
.app
.name
, port
=PORT
)
210 token
= self
._base
64_encode
(
211 f
"{config.web_config_username}:{config.web_config_password}"
213 container_builder
.add_http_readiness_probe(
216 initial_delay_seconds
=10,
218 http_headers
=[("Authorization", f
"Basic {token}")],
220 container_builder
.add_http_liveness_probe(
223 initial_delay_seconds
=30,
225 http_headers
=[("Authorization", f
"Basic {token}")],
229 "--config.file=/etc/prometheus/prometheus.yml",
230 "--web.config.file=/etc/prometheus/web-config/web.yml",
231 "--storage.tsdb.path=/prometheus",
232 "--web.console.libraries=/usr/share/prometheus/console_libraries",
233 "--web.console.templates=/usr/share/prometheus/consoles",
234 f
"--web.route-prefix={config.web_subpath}",
235 f
"--web.external-url=http://localhost:{PORT}{config.web_subpath}",
237 if config
.enable_web_admin_api
:
238 command
.append("--web.enable-admin-api")
239 container_builder
.add_command(command
)
240 container_builder
.add_volume_config(
241 "config", "/etc/prometheus", self
._build
_config
_file
(config
)
243 container_builder
.add_volume_config(
245 "/etc/prometheus/web-config",
246 self
._build
_webconfig
_file
(),
247 secret_name
=prometheus_secret_name
,
249 container
= container_builder
.build()
250 # Add container to pod spec
251 pod_spec_builder
.add_container(container
)
252 # Add ingress resources to pod spec if site url exists
254 parsed
= urlparse(config
.site_url
)
256 "nginx.ingress.kubernetes.io/proxy-body-size": "{}".format(
257 str(config
.max_file_size
) + "m"
258 if config
.max_file_size
> 0
259 else config
.max_file_size
262 if config
.ingress_class
:
263 annotations
["kubernetes.io/ingress.class"] = config
.ingress_class
264 ingress_resource_builder
= IngressResourceV3Builder(
265 f
"{self.app.name}-ingress", annotations
268 if config
.ingress_whitelist_source_range
:
270 "nginx.ingress.kubernetes.io/whitelist-source-range"
271 ] = config
.ingress_whitelist_source_range
273 if config
.cluster_issuer
:
274 annotations
["cert-manager.io/cluster-issuer"] = config
.cluster_issuer
276 if parsed
.scheme
== "https":
277 ingress_resource_builder
.add_tls(
278 [parsed
.hostname
], config
.tls_secret_name
281 annotations
["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
283 ingress_resource_builder
.add_rule(parsed
.hostname
, self
.app
.name
, PORT
)
284 ingress_resource
= ingress_resource_builder
.build()
285 pod_spec_builder
.add_ingress_resource(ingress_resource
)
286 return pod_spec_builder
.build()
288 def _hash_password(self
, password
):
289 hashed_password
= bcrypt
.hashpw(password
.encode("utf-8"), bcrypt
.gensalt())
290 return hashed_password
.decode()
292 def _base64_encode(self
, phrase
: str) -> str:
293 return base64
.b64encode(phrase
.encode("utf-8")).decode("utf-8")
296 if __name__
== "__main__":
297 main(PrometheusCharm
)