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 pathlib
import Path
28 from typing
import NoReturn
, Optional
29 from urllib
.parse
import urlparse
31 from ops
.main
import main
32 from opslib
.osm
.charm
import CharmedOsmBase
, RelationsMissing
33 from opslib
.osm
.interfaces
.grafana
import GrafanaDashboardTarget
34 from opslib
.osm
.interfaces
.mysql
import MysqlClient
35 from opslib
.osm
.interfaces
.prometheus
import PrometheusScrapeTarget
36 from opslib
.osm
.pod
import (
38 IngressResourceV3Builder
,
41 from opslib
.osm
.validator
import ModelValidator
, validator
44 logger
= logging
.getLogger(__name__
)
49 class ConfigModel(ModelValidator
):
50 site_url
: Optional
[str]
51 cluster_issuer
: Optional
[str]
52 ingress_class
: Optional
[str]
53 ingress_whitelist_source_range
: Optional
[str]
54 tls_secret_name
: Optional
[str]
55 mysql_uri
: Optional
[str]
57 @validator("site_url")
58 def validate_site_url(cls
, v
):
61 if not parsed
.scheme
.startswith("http"):
62 raise ValueError("value must start with http")
65 @validator("ingress_whitelist_source_range")
66 def validate_ingress_whitelist_source_range(cls
, v
):
71 @validator("mysql_uri")
72 def validate_mysql_uri(cls
, v
):
73 if v
and not v
.startswith("mysql://"):
74 raise ValueError("mysql_uri is not properly formed")
78 class MysqlExporterCharm(CharmedOsmBase
):
79 def __init__(self
, *args
) -> NoReturn
:
80 super().__init
__(*args
, oci_image
="image")
82 # Provision Kafka relation to exchange information
83 self
.mysql_client
= MysqlClient(self
, "mysql")
84 self
.framework
.observe(self
.on
["mysql"].relation_changed
, self
.configure_pod
)
85 self
.framework
.observe(self
.on
["mysql"].relation_broken
, self
.configure_pod
)
87 # Register relation to provide a Scraping Target
88 self
.scrape_target
= PrometheusScrapeTarget(self
, "prometheus-scrape")
89 self
.framework
.observe(
90 self
.on
["prometheus-scrape"].relation_joined
, self
._publish
_scrape
_info
93 # Register relation to provide a Dasboard Target
94 self
.dashboard_target
= GrafanaDashboardTarget(self
, "grafana-dashboard")
95 self
.framework
.observe(
96 self
.on
["grafana-dashboard"].relation_joined
, self
._publish
_dashboard
_info
99 def _publish_scrape_info(self
, event
) -> NoReturn
:
100 """Publishes scraping information for Prometheus.
103 event (EventBase): Prometheus relation event.
105 if self
.unit
.is_leader():
107 urlparse(self
.model
.config
["site_url"]).hostname
108 if self
.model
.config
["site_url"]
109 else self
.model
.app
.name
112 if self
.model
.config
.get("site_url", "").startswith("https://"):
114 elif self
.model
.config
.get("site_url", "").startswith("http://"):
117 self
.scrape_target
.publish_info(
120 metrics_path
="/metrics",
121 scrape_interval
="30s",
122 scrape_timeout
="15s",
125 def _publish_dashboard_info(self
, event
) -> NoReturn
:
126 """Publish dashboards for Grafana.
129 event (EventBase): Grafana relation event.
131 if self
.unit
.is_leader():
132 self
.dashboard_target
.publish_info(
134 dashboard
=Path("files/mysql_exporter_dashboard.json").read_text(),
137 def _check_missing_dependencies(self
, config
: ConfigModel
):
138 """Check if there is any relation missing.
141 config (ConfigModel): object with configuration information.
144 RelationsMissing: if kafka is missing.
146 missing_relations
= []
148 if not config
.mysql_uri
and self
.mysql_client
.is_missing_data_in_unit():
149 missing_relations
.append("mysql")
151 if missing_relations
:
152 raise RelationsMissing(missing_relations
)
154 def build_pod_spec(self
, image_info
):
155 """Build the PodSpec to be used.
158 image_info (str): container image information.
161 Dict: PodSpec information.
164 config
= ConfigModel(**dict(self
.config
))
166 if config
.mysql_uri
and not self
.mysql_client
.is_missing_data_in_unit():
167 raise Exception("Mysql data cannot be provided via config and relation")
170 self
._check
_missing
_dependencies
(config
)
172 # Create Builder for the PodSpec
173 pod_spec_builder
= PodSpecV3Builder()
176 container_builder
= ContainerV3Builder(self
.app
.name
, image_info
)
177 container_builder
.add_port(name
=self
.app
.name
, port
=PORT
)
178 container_builder
.add_http_readiness_probe(
181 initial_delay_seconds
=10,
187 container_builder
.add_http_liveness_probe(
190 initial_delay_seconds
=60,
192 failure_threshold
=10,
196 config
.mysql_uri
.replace("mysql://", "").split("/")[0]
198 else f
"root:{self.mysql_client.root_password}@{self.mysql_client.host}:{self.mysql_client.port}"
201 container_builder
.add_envs(
203 "DATA_SOURCE_NAME": data_source
,
206 container
= container_builder
.build()
208 # Add container to PodSpec
209 pod_spec_builder
.add_container(container
)
211 # Add ingress resources to PodSpec if site url exists
213 parsed
= urlparse(config
.site_url
)
215 if config
.ingress_class
:
216 annotations
["kubernetes.io/ingress.class"] = config
.ingress_class
217 ingress_resource_builder
= IngressResourceV3Builder(
218 f
"{self.app.name}-ingress", annotations
221 if config
.ingress_whitelist_source_range
:
223 "nginx.ingress.kubernetes.io/whitelist-source-range"
224 ] = config
.ingress_whitelist_source_range
226 if config
.cluster_issuer
:
227 annotations
["cert-manager.io/cluster-issuer"] = config
.cluster_issuer
229 if parsed
.scheme
== "https":
230 ingress_resource_builder
.add_tls(
231 [parsed
.hostname
], config
.tls_secret_name
234 annotations
["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
236 ingress_resource_builder
.add_rule(parsed
.hostname
, self
.app
.name
, PORT
)
237 ingress_resource
= ingress_resource_builder
.build()
238 pod_spec_builder
.add_ingress_resource(ingress_resource
)
240 logger
.debug(pod_spec_builder
.build())
242 return pod_spec_builder
.build()
245 if __name__
== "__main__":
246 main(MysqlExporterCharm
)