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
25 from ipaddress
import ip_network
27 from typing
import NoReturn
, Optional
28 from urllib
.parse
import urlparse
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 (
38 IngressResourceV3Builder
,
41 from opslib
.osm
.validator
import (
48 logger
= logging
.getLogger(__name__
)
53 class ConfigModel(ModelValidator
):
57 site_url
: Optional
[str]
58 cluster_issuer
: Optional
[str]
59 ingress_class
: Optional
[str]
60 ingress_whitelist_source_range
: Optional
[str]
61 tls_secret_name
: Optional
[str]
62 enable_web_admin_api
: bool
64 @validator("web_subpath")
65 def validate_web_subpath(cls
, v
):
67 raise ValueError("web-subpath must be a non-empty string")
70 @validator("max_file_size")
71 def validate_max_file_size(cls
, v
):
73 raise ValueError("value must be equal or greater than 0")
76 @validator("site_url")
77 def validate_site_url(cls
, v
):
80 if not parsed
.scheme
.startswith("http"):
81 raise ValueError("value must start with http")
84 @validator("ingress_whitelist_source_range")
85 def validate_ingress_whitelist_source_range(cls
, v
):
91 class PrometheusCharm(CharmedOsmBase
):
93 """Prometheus Charm."""
95 def __init__(self
, *args
) -> NoReturn
:
96 """Prometheus Charm constructor."""
97 super().__init
__(*args
, oci_image
="image")
99 # Registering provided relation events
100 self
.prometheus
= PrometheusServer(self
, "prometheus")
101 self
.framework
.observe(
102 self
.on
.prometheus_relation_joined
, # pylint: disable=E1101
103 self
._publish
_prometheus
_info
,
106 # Registering actions
107 self
.framework
.observe(
108 self
.on
.backup_action
, # pylint: disable=E1101
109 self
._on
_backup
_action
,
112 def _publish_prometheus_info(self
, event
: EventBase
) -> NoReturn
:
113 self
.prometheus
.publish_info(self
.app
.name
, PORT
)
115 def _on_backup_action(self
, event
: EventBase
) -> NoReturn
:
116 url
= f
"http://{self.model.app.name}:{PORT}/api/v1/admin/tsdb/snapshot"
117 result
= requests
.post(url
)
119 if result
.status_code
== 200:
120 event
.set_results({"backup-name": result
.json()["name"]})
122 event
.fail(f
"status-code: {result.status_code}")
124 def _build_files(self
, config
: ConfigModel
):
125 files_builder
= FilesV3Builder()
126 files_builder
.add_file(
130 " scrape_interval: 15s\n"
131 " evaluation_interval: 15s\n"
134 " - static_configs:\n"
138 " - job_name: 'prometheus'\n"
140 f
" - targets: [{config.default_target}]\n"
143 return files_builder
.build()
145 def build_pod_spec(self
, image_info
):
147 config
= ConfigModel(**dict(self
.config
))
148 # Create Builder for the PodSpec
149 pod_spec_builder
= PodSpecV3Builder()
151 # Build Backup Container
152 backup_image
= OCIImageResource(self
, "backup-image")
153 backup_image_info
= backup_image
.fetch()
154 backup_container_builder
= ContainerV3Builder("prom-backup", backup_image_info
)
155 backup_container
= backup_container_builder
.build()
156 # Add backup container to pod spec
157 pod_spec_builder
.add_container(backup_container
)
160 container_builder
= ContainerV3Builder(self
.app
.name
, image_info
)
161 container_builder
.add_port(name
=self
.app
.name
, port
=PORT
)
162 container_builder
.add_http_readiness_probe(
165 initial_delay_seconds
=10,
168 container_builder
.add_http_liveness_probe(
171 initial_delay_seconds
=30,
176 "--config.file=/etc/prometheus/prometheus.yml",
177 "--storage.tsdb.path=/prometheus",
178 "--web.console.libraries=/usr/share/prometheus/console_libraries",
179 "--web.console.templates=/usr/share/prometheus/consoles",
180 f
"--web.route-prefix={config.web_subpath}",
181 f
"--web.external-url=http://localhost:{PORT}{config.web_subpath}",
183 if config
.enable_web_admin_api
:
184 command
.append("--web.enable-admin-api")
185 container_builder
.add_command(command
)
186 container_builder
.add_volume_config(
187 "config", "/etc/prometheus", self
._build
_files
(config
)
189 container
= container_builder
.build()
190 # Add container to pod spec
191 pod_spec_builder
.add_container(container
)
192 # Add ingress resources to pod spec if site url exists
194 parsed
= urlparse(config
.site_url
)
196 "nginx.ingress.kubernetes.io/proxy-body-size": "{}".format(
197 str(config
.max_file_size
) + "m"
198 if config
.max_file_size
> 0
199 else config
.max_file_size
202 if config
.ingress_class
:
203 annotations
["kubernetes.io/ingress.class"] = config
.ingress_class
204 ingress_resource_builder
= IngressResourceV3Builder(
205 f
"{self.app.name}-ingress", annotations
208 if config
.ingress_whitelist_source_range
:
210 "nginx.ingress.kubernetes.io/whitelist-source-range"
211 ] = config
.ingress_whitelist_source_range
213 if config
.cluster_issuer
:
214 annotations
["cert-manager.io/cluster-issuer"] = config
.cluster_issuer
216 if parsed
.scheme
== "https":
217 ingress_resource_builder
.add_tls(
218 [parsed
.hostname
], config
.tls_secret_name
221 annotations
["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
223 ingress_resource_builder
.add_rule(parsed
.hostname
, self
.app
.name
, PORT
)
224 ingress_resource
= ingress_resource_builder
.build()
225 pod_spec_builder
.add_ingress_resource(ingress_resource
)
226 return pod_spec_builder
.build()
229 if __name__
== "__main__":
230 main(PrometheusCharm
)