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 charms
.kafka_k8s
.v0
.kafka
import KafkaEvents
, KafkaRequires
32 from ops
.main
import main
33 from opslib
.osm
.charm
import CharmedOsmBase
, RelationsMissing
34 from opslib
.osm
.interfaces
.grafana
import GrafanaDashboardTarget
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 image_pull_policy
: str
56 security_context
: bool
58 @validator("site_url")
59 def validate_site_url(cls
, v
):
62 if not parsed
.scheme
.startswith("http"):
63 raise ValueError("value must start with http")
66 @validator("ingress_whitelist_source_range")
67 def validate_ingress_whitelist_source_range(cls
, v
):
72 @validator("image_pull_policy")
73 def validate_image_pull_policy(cls
, v
):
76 "ifnotpresent": "IfNotPresent",
80 if v
not in values
.keys():
81 raise ValueError("value must be always, ifnotpresent or never")
85 class KafkaExporterCharm(CharmedOsmBase
):
89 def __init__(self
, *args
) -> NoReturn
:
90 super().__init
__(*args
, oci_image
="image")
92 # Provision Kafka relation to exchange information
93 self
.kafka
= KafkaRequires(self
)
94 self
.framework
.observe(self
.on
.kafka_available
, self
.configure_pod
)
95 self
.framework
.observe(self
.on
.kafka_broken
, self
.configure_pod
)
97 # Register relation to provide a Scraping Target
98 self
.scrape_target
= PrometheusScrapeTarget(self
, "prometheus-scrape")
99 self
.framework
.observe(
100 self
.on
["prometheus-scrape"].relation_joined
, self
._publish
_scrape
_info
103 # Register relation to provide a Dasboard Target
104 self
.dashboard_target
= GrafanaDashboardTarget(self
, "grafana-dashboard")
105 self
.framework
.observe(
106 self
.on
["grafana-dashboard"].relation_joined
, self
._publish
_dashboard
_info
109 def _publish_scrape_info(self
, event
) -> NoReturn
:
110 """Publishes scraping information for Prometheus.
113 event (EventBase): Prometheus relation event.
115 if self
.unit
.is_leader():
117 urlparse(self
.model
.config
["site_url"]).hostname
118 if self
.model
.config
["site_url"]
119 else self
.model
.app
.name
122 if self
.model
.config
.get("site_url", "").startswith("https://"):
124 elif self
.model
.config
.get("site_url", "").startswith("http://"):
127 self
.scrape_target
.publish_info(
130 metrics_path
="/metrics",
131 scrape_interval
="30s",
132 scrape_timeout
="15s",
135 def _publish_dashboard_info(self
, event
) -> NoReturn
:
136 """Publish dashboards for Grafana.
139 event (EventBase): Grafana relation event.
141 if self
.unit
.is_leader():
142 self
.dashboard_target
.publish_info(
144 dashboard
=Path("templates/kafka_exporter_dashboard.json").read_text(),
147 def _check_missing_dependencies(self
, config
: ConfigModel
):
148 """Check if there is any relation missing.
151 config (ConfigModel): object with configuration information.
154 RelationsMissing: if kafka is missing.
156 missing_relations
= []
158 if not self
.kafka
.host
or not self
.kafka
.port
:
159 missing_relations
.append("kafka")
161 if missing_relations
:
162 raise RelationsMissing(missing_relations
)
164 def build_pod_spec(self
, image_info
):
165 """Build the PodSpec to be used.
168 image_info (str): container image information.
171 Dict: PodSpec information.
174 config
= ConfigModel(**dict(self
.config
))
177 self
._check
_missing
_dependencies
(config
)
179 # Create Builder for the PodSpec
180 pod_spec_builder
= PodSpecV3Builder(
181 enable_security_context
=config
.security_context
185 container_builder
= ContainerV3Builder(
188 config
.image_pull_policy
,
189 run_as_non_root
=config
.security_context
,
191 container_builder
.add_port(name
=self
.app
.name
, port
=PORT
)
192 container_builder
.add_http_readiness_probe(
195 initial_delay_seconds
=10,
201 container_builder
.add_http_liveness_probe(
204 initial_delay_seconds
=60,
206 failure_threshold
=10,
208 container_builder
.add_command(
211 f
"--kafka.server={self.kafka.host}:{self.kafka.port}",
214 container
= container_builder
.build()
216 # Add container to PodSpec
217 pod_spec_builder
.add_container(container
)
219 # Add ingress resources to PodSpec if site url exists
221 parsed
= urlparse(config
.site_url
)
223 if config
.ingress_class
:
224 annotations
["kubernetes.io/ingress.class"] = config
.ingress_class
225 ingress_resource_builder
= IngressResourceV3Builder(
226 f
"{self.app.name}-ingress", annotations
229 if config
.ingress_whitelist_source_range
:
231 "nginx.ingress.kubernetes.io/whitelist-source-range"
232 ] = config
.ingress_whitelist_source_range
234 if config
.cluster_issuer
:
235 annotations
["cert-manager.io/cluster-issuer"] = config
.cluster_issuer
237 if parsed
.scheme
== "https":
238 ingress_resource_builder
.add_tls(
239 [parsed
.hostname
], config
.tls_secret_name
242 annotations
["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
244 ingress_resource_builder
.add_rule(parsed
.hostname
, self
.app
.name
, PORT
)
245 ingress_resource
= ingress_resource_builder
.build()
246 pod_spec_builder
.add_ingress_resource(ingress_resource
)
248 return pod_spec_builder
.build()
251 if __name__
== "__main__":
252 main(KafkaExporterCharm
)