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 typing
import Optional
, NoReturn
27 from ipaddress
import ip_network
29 from ops
.main
import main
31 from opslib
.osm
.charm
import CharmedOsmBase
, RelationsMissing
33 from opslib
.osm
.pod
import (
34 IngressResourceV3Builder
,
41 from opslib
.osm
.validator
import (
46 from opslib
.osm
.interfaces
.prometheus
import PrometheusClient
48 from urllib
.parse
import urlparse
49 from string
import Template
50 from pathlib
import Path
52 logger
= logging
.getLogger(__name__
)
57 class ConfigModel(ModelValidator
):
60 site_url
: Optional
[str]
61 ingress_whitelist_source_range
: Optional
[str]
62 tls_secret_name
: Optional
[str]
64 @validator("max_file_size")
65 def validate_max_file_size(cls
, v
):
67 raise ValueError("value must be equal or greater than 0")
70 @validator("site_url")
71 def validate_site_url(cls
, v
):
74 if not parsed
.scheme
.startswith("http"):
75 raise ValueError("value must start with http")
78 @validator("ingress_whitelist_source_range")
79 def validate_ingress_whitelist_source_range(cls
, v
):
85 class GrafanaCharm(CharmedOsmBase
):
86 """GrafanaCharm Charm."""
88 def __init__(self
, *args
) -> NoReturn
:
89 """Prometheus Charm constructor."""
90 super().__init
__(*args
, oci_image
="image")
92 self
.prometheus_client
= PrometheusClient(self
, "prometheus")
93 self
.framework
.observe(self
.on
["prometheus"].relation_changed
, self
.configure_pod
)
94 self
.framework
.observe(self
.on
["prometheus"].relation_broken
, self
.configure_pod
)
96 def _build_dashboard_files(self
, config
: ConfigModel
):
97 files_builder
= FilesV3Builder()
98 files_builder
.add_file(
100 Path("files/default_dashboards.yaml").read_text(),
102 if config
.osm_dashboards
:
103 osm_dashboards_mapping
= {
104 "kafka_exporter_dashboard.json": "files/kafka_exporter_dashboard.json",
105 "mongodb_exporter_dashboard.json": "files/mongodb_exporter_dashboard.json",
106 "mysql_exporter_dashboard.json": "files/mysql_exporter_dashboard.json",
107 "nodes_exporter_dashboard.json": "files/nodes_exporter_dashboard.json",
108 "summary_dashboard.json": "files/summary_dashboard.json",
110 for file_name
, path
in osm_dashboards_mapping
.items():
111 files_builder
.add_file(file_name
, Path(path
).read_text())
112 return files_builder
.build()
114 def _build_datasources_files(self
):
115 files_builder
= FilesV3Builder()
116 files_builder
.add_file(
117 "datasource_prometheus.yaml",
118 Template(Path("files/default_datasources.yaml").read_text()).substitute(
119 prometheus_host
=self
.prometheus_client
.hostname
,
120 prometheus_port
=self
.prometheus_client
.port
,
123 return files_builder
.build()
125 def _check_missing_dependencies(self
):
126 missing_relations
= []
128 if self
.prometheus_client
.is_missing_data_in_app():
129 missing_relations
.append("prometheus")
131 if missing_relations
:
132 raise RelationsMissing(missing_relations
)
134 def build_pod_spec(self
, image_info
):
136 config
= ConfigModel(**dict(self
.config
))
138 self
._check
_missing
_dependencies
()
139 # Create Builder for the PodSpec
140 pod_spec_builder
= PodSpecV3Builder()
142 container_builder
= ContainerV3Builder(self
.app
.name
, image_info
)
143 container_builder
.add_port(name
=self
.app
.name
, port
=PORT
)
144 container_builder
.add_http_readiness_probe(
147 initial_delay_seconds
=10,
152 container_builder
.add_http_liveness_probe(
155 initial_delay_seconds
=60,
157 failure_threshold
=10,
159 container_builder
.add_volume_config(
161 "/etc/grafana/provisioning/dashboards/",
162 self
._build
_dashboard
_files
(config
),
164 container_builder
.add_volume_config(
166 "/etc/grafana/provisioning/datasources/",
167 self
._build
_datasources
_files
(),
169 container
= container_builder
.build()
170 # Add container to pod spec
171 pod_spec_builder
.add_container(container
)
172 # Add ingress resources to pod spec if site url exists
174 parsed
= urlparse(config
.site_url
)
176 "nginx.ingress.kubernetes.io/proxy-body-size": "{}".format(
177 str(config
.max_file_size
) + "m"
178 if config
.max_file_size
> 0
179 else config
.max_file_size
182 ingress_resource_builder
= IngressResourceV3Builder(
183 f
"{self.app.name}-ingress", annotations
186 if config
.ingress_whitelist_source_range
:
188 "nginx.ingress.kubernetes.io/whitelist-source-range"
189 ] = config
.ingress_whitelist_source_range
191 if parsed
.scheme
== "https":
192 ingress_resource_builder
.add_tls(
193 [parsed
.hostname
], config
.tls_secret_name
196 annotations
["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
198 ingress_resource_builder
.add_rule(parsed
.hostname
, self
.app
.name
, PORT
)
199 ingress_resource
= ingress_resource_builder
.build()
200 pod_spec_builder
.add_ingress_resource(ingress_resource
)
201 return pod_spec_builder
.build()
204 if __name__
== "__main__":