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
,
42 from opslib
.osm
.validator
import ModelValidator
, validator
45 logger
= logging
.getLogger(__name__
)
50 class ConfigModel(ModelValidator
):
51 site_url
: Optional
[str]
52 cluster_issuer
: Optional
[str]
53 ingress_class
: Optional
[str]
54 ingress_whitelist_source_range
: Optional
[str]
55 tls_secret_name
: Optional
[str]
56 mysql_uri
: Optional
[str]
57 image_pull_policy
: str
59 @validator("site_url")
60 def validate_site_url(cls
, v
):
63 if not parsed
.scheme
.startswith("http"):
64 raise ValueError("value must start with http")
67 @validator("ingress_whitelist_source_range")
68 def validate_ingress_whitelist_source_range(cls
, v
):
73 @validator("mysql_uri")
74 def validate_mysql_uri(cls
, v
):
75 if v
and not v
.startswith("mysql://"):
76 raise ValueError("mysql_uri is not properly formed")
79 @validator("image_pull_policy")
80 def validate_image_pull_policy(cls
, v
):
83 "ifnotpresent": "IfNotPresent",
87 if v
not in values
.keys():
88 raise ValueError("value must be always, ifnotpresent or never")
92 class MysqlExporterCharm(CharmedOsmBase
):
93 def __init__(self
, *args
) -> NoReturn
:
94 super().__init
__(*args
, oci_image
="image")
96 # Provision Kafka relation to exchange information
97 self
.mysql_client
= MysqlClient(self
, "mysql")
98 self
.framework
.observe(self
.on
["mysql"].relation_changed
, self
.configure_pod
)
99 self
.framework
.observe(self
.on
["mysql"].relation_broken
, self
.configure_pod
)
101 # Register relation to provide a Scraping Target
102 self
.scrape_target
= PrometheusScrapeTarget(self
, "prometheus-scrape")
103 self
.framework
.observe(
104 self
.on
["prometheus-scrape"].relation_joined
, self
._publish
_scrape
_info
107 # Register relation to provide a Dasboard Target
108 self
.dashboard_target
= GrafanaDashboardTarget(self
, "grafana-dashboard")
109 self
.framework
.observe(
110 self
.on
["grafana-dashboard"].relation_joined
, self
._publish
_dashboard
_info
113 def _publish_scrape_info(self
, event
) -> NoReturn
:
114 """Publishes scraping information for Prometheus.
117 event (EventBase): Prometheus relation event.
119 if self
.unit
.is_leader():
121 urlparse(self
.model
.config
["site_url"]).hostname
122 if self
.model
.config
["site_url"]
123 else self
.model
.app
.name
126 if self
.model
.config
.get("site_url", "").startswith("https://"):
128 elif self
.model
.config
.get("site_url", "").startswith("http://"):
131 self
.scrape_target
.publish_info(
134 metrics_path
="/metrics",
135 scrape_interval
="30s",
136 scrape_timeout
="15s",
139 def _publish_dashboard_info(self
, event
) -> NoReturn
:
140 """Publish dashboards for Grafana.
143 event (EventBase): Grafana relation event.
145 if self
.unit
.is_leader():
146 self
.dashboard_target
.publish_info(
148 dashboard
=Path("templates/mysql_exporter_dashboard.json").read_text(),
151 def _check_missing_dependencies(self
, config
: ConfigModel
):
152 """Check if there is any relation missing.
155 config (ConfigModel): object with configuration information.
158 RelationsMissing: if kafka is missing.
160 missing_relations
= []
162 if not config
.mysql_uri
and self
.mysql_client
.is_missing_data_in_unit():
163 missing_relations
.append("mysql")
165 if missing_relations
:
166 raise RelationsMissing(missing_relations
)
168 def build_pod_spec(self
, image_info
):
169 """Build the PodSpec to be used.
172 image_info (str): container image information.
175 Dict: PodSpec information.
178 config
= ConfigModel(**dict(self
.config
))
180 if config
.mysql_uri
and not self
.mysql_client
.is_missing_data_in_unit():
181 raise Exception("Mysql data cannot be provided via config and relation")
184 self
._check
_missing
_dependencies
(config
)
187 config
.mysql_uri
.replace("mysql://", "").split("/")[0]
189 else f
"root:{self.mysql_client.root_password}@{self.mysql_client.host}:{self.mysql_client.port}"
192 # Create Builder for the PodSpec
193 pod_spec_builder
= PodSpecV3Builder()
195 # Add secrets to the pod
196 mysql_secret_name
= f
"{self.app.name}-mysql-secret"
197 pod_spec_builder
.add_secret(
199 {"data_source": data_source
},
203 container_builder
= ContainerV3Builder(
204 self
.app
.name
, image_info
, config
.image_pull_policy
206 container_builder
.add_port(name
=self
.app
.name
, port
=PORT
)
207 container_builder
.add_http_readiness_probe(
210 initial_delay_seconds
=10,
216 container_builder
.add_http_liveness_probe(
219 initial_delay_seconds
=60,
221 failure_threshold
=10,
223 container_builder
.add_secret_envs(
224 mysql_secret_name
, {"DATA_SOURCE_NAME": "data_source"}
227 container
= container_builder
.build()
229 # Add container to PodSpec
230 pod_spec_builder
.add_container(container
)
232 # Add Pod restart policy
233 restart_policy
= PodRestartPolicy()
234 restart_policy
.add_secrets(secret_names
=(mysql_secret_name
))
235 pod_spec_builder
.set_restart_policy(restart_policy
)
237 # Add ingress resources to PodSpec if site url exists
239 parsed
= urlparse(config
.site_url
)
241 if config
.ingress_class
:
242 annotations
["kubernetes.io/ingress.class"] = config
.ingress_class
243 ingress_resource_builder
= IngressResourceV3Builder(
244 f
"{self.app.name}-ingress", annotations
247 if config
.ingress_whitelist_source_range
:
249 "nginx.ingress.kubernetes.io/whitelist-source-range"
250 ] = config
.ingress_whitelist_source_range
252 if config
.cluster_issuer
:
253 annotations
["cert-manager.io/cluster-issuer"] = config
.cluster_issuer
255 if parsed
.scheme
== "https":
256 ingress_resource_builder
.add_tls(
257 [parsed
.hostname
], config
.tls_secret_name
260 annotations
["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
262 ingress_resource_builder
.add_rule(parsed
.hostname
, self
.app
.name
, PORT
)
263 ingress_resource
= ingress_resource_builder
.build()
264 pod_spec_builder
.add_ingress_resource(ingress_resource
)
266 return pod_spec_builder
.build()
269 if __name__
== "__main__":
270 main(MysqlExporterCharm
)