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
.mongo
import MongoClient
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 mongodb_uri
: Optional
[str]
57 image_pull_policy
: str
58 security_context
: bool
60 @validator("site_url")
61 def validate_site_url(cls
, v
):
64 if not parsed
.scheme
.startswith("http"):
65 raise ValueError("value must start with http")
68 @validator("ingress_whitelist_source_range")
69 def validate_ingress_whitelist_source_range(cls
, v
):
74 @validator("mongodb_uri")
75 def validate_mongodb_uri(cls
, v
):
76 if v
and not v
.startswith("mongodb://"):
77 raise ValueError("mongodb_uri is not properly formed")
80 @validator("image_pull_policy")
81 def validate_image_pull_policy(cls
, v
):
84 "ifnotpresent": "IfNotPresent",
88 if v
not in values
.keys():
89 raise ValueError("value must be always, ifnotpresent or never")
93 class MongodbExporterCharm(CharmedOsmBase
):
94 def __init__(self
, *args
) -> NoReturn
:
95 super().__init
__(*args
, oci_image
="image")
97 # Provision Kafka relation to exchange information
98 self
.mongodb_client
= MongoClient(self
, "mongodb")
99 self
.framework
.observe(self
.on
["mongodb"].relation_changed
, self
.configure_pod
)
100 self
.framework
.observe(self
.on
["mongodb"].relation_broken
, self
.configure_pod
)
102 # Register relation to provide a Scraping Target
103 self
.scrape_target
= PrometheusScrapeTarget(self
, "prometheus-scrape")
104 self
.framework
.observe(
105 self
.on
["prometheus-scrape"].relation_joined
, self
._publish
_scrape
_info
108 # Register relation to provide a Dasboard Target
109 self
.dashboard_target
= GrafanaDashboardTarget(self
, "grafana-dashboard")
110 self
.framework
.observe(
111 self
.on
["grafana-dashboard"].relation_joined
, self
._publish
_dashboard
_info
114 def _publish_scrape_info(self
, event
) -> NoReturn
:
115 """Publishes scraping information for Prometheus.
118 event (EventBase): Prometheus relation event.
120 if self
.unit
.is_leader():
122 urlparse(self
.model
.config
["site_url"]).hostname
123 if self
.model
.config
["site_url"]
124 else self
.model
.app
.name
127 if self
.model
.config
.get("site_url", "").startswith("https://"):
129 elif self
.model
.config
.get("site_url", "").startswith("http://"):
132 self
.scrape_target
.publish_info(
135 metrics_path
="/metrics",
136 scrape_interval
="30s",
137 scrape_timeout
="15s",
140 def _publish_dashboard_info(self
, event
) -> NoReturn
:
141 """Publish dashboards for Grafana.
144 event (EventBase): Grafana relation event.
146 if self
.unit
.is_leader():
147 self
.dashboard_target
.publish_info(
149 dashboard
=Path("templates/mongodb_exporter_dashboard.json").read_text(),
152 def _check_missing_dependencies(self
, config
: ConfigModel
):
153 """Check if there is any relation missing.
156 config (ConfigModel): object with configuration information.
159 RelationsMissing: if kafka is missing.
161 missing_relations
= []
163 if not config
.mongodb_uri
and self
.mongodb_client
.is_missing_data_in_unit():
164 missing_relations
.append("mongodb")
166 if missing_relations
:
167 raise RelationsMissing(missing_relations
)
169 def build_pod_spec(self
, image_info
):
170 """Build the PodSpec to be used.
173 image_info (str): container image information.
176 Dict: PodSpec information.
179 config
= ConfigModel(**dict(self
.config
))
181 if config
.mongodb_uri
and not self
.mongodb_client
.is_missing_data_in_unit():
182 raise Exception("Mongodb data cannot be provided via config and relation")
185 self
._check
_missing
_dependencies
(config
)
189 if config
.mongodb_uri
190 else self
.mongodb_client
.connection_string
192 parsed
= urlparse(unparsed
)
193 mongodb_uri
= f
"mongodb://{parsed.netloc.split(',')[0]}{parsed.path}"
195 mongodb_uri
+= f
"?{parsed.query}"
197 # Create Builder for the PodSpec
198 pod_spec_builder
= PodSpecV3Builder(
199 enable_security_context
=config
.security_context
202 # Add secrets to the pod
203 mongodb_secret_name
= f
"{self.app.name}-mongodb-secret"
204 pod_spec_builder
.add_secret(mongodb_secret_name
, {"uri": mongodb_uri
})
207 container_builder
= ContainerV3Builder(
210 config
.image_pull_policy
,
211 run_as_non_root
=config
.security_context
,
213 container_builder
.add_port(name
="exporter", port
=PORT
)
214 container_builder
.add_http_readiness_probe(
217 initial_delay_seconds
=10,
223 container_builder
.add_http_liveness_probe(
226 initial_delay_seconds
=60,
228 failure_threshold
=10,
231 container_builder
.add_secret_envs(mongodb_secret_name
, {"MONGODB_URI": "uri"})
232 container
= container_builder
.build()
234 # Add container to PodSpec
235 pod_spec_builder
.add_container(container
)
237 # Add Pod restart policy
238 restart_policy
= PodRestartPolicy()
239 restart_policy
.add_secrets(secret_names
=(mongodb_secret_name
,))
240 pod_spec_builder
.set_restart_policy(restart_policy
)
242 # Add ingress resources to PodSpec if site url exists
244 parsed
= urlparse(config
.site_url
)
246 if config
.ingress_class
:
247 annotations
["kubernetes.io/ingress.class"] = config
.ingress_class
248 ingress_resource_builder
= IngressResourceV3Builder(
249 f
"{self.app.name}-ingress", annotations
252 if config
.ingress_whitelist_source_range
:
254 "nginx.ingress.kubernetes.io/whitelist-source-range"
255 ] = config
.ingress_whitelist_source_range
257 if config
.cluster_issuer
:
258 annotations
["cert-manager.io/cluster-issuer"] = config
.cluster_issuer
260 if parsed
.scheme
== "https":
261 ingress_resource_builder
.add_tls(
262 [parsed
.hostname
], config
.tls_secret_name
265 annotations
["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
267 ingress_resource_builder
.add_rule(parsed
.hostname
, self
.app
.name
, PORT
)
268 ingress_resource
= ingress_resource_builder
.build()
269 pod_spec_builder
.add_ingress_resource(ingress_resource
)
271 return pod_spec_builder
.build()
274 if __name__
== "__main__":
275 main(MongodbExporterCharm
)