Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1#!/usr/bin/env python3 

2# Copyright 2021 Canonical Ltd. 

3# 

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 

7# 

8# http://www.apache.org/licenses/LICENSE-2.0 

9# 

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 

14# under the License. 

15# 

16# For those usages not covered by the Apache License, Version 2.0 please 

17# contact: legal@canonical.com 

18# 

19# To get in touch with the maintainers, please contact: 

20# osm-charmers@lists.launchpad.net 

21## 

22 

23# pylint: disable=E0213 

24 

25from ipaddress import ip_network 

26import logging 

27from pathlib import Path 

28from string import Template 

29from typing import NoReturn, Optional 

30from urllib.parse import urlparse 

31 

32from ops.main import main 

33from opslib.osm.charm import CharmedOsmBase, RelationsMissing 

34from opslib.osm.interfaces.prometheus import PrometheusClient 

35from opslib.osm.pod import ( 

36 ContainerV3Builder, 

37 FilesV3Builder, 

38 IngressResourceV3Builder, 

39 PodSpecV3Builder, 

40) 

41from opslib.osm.validator import ModelValidator, validator 

42 

43 

44logger = logging.getLogger(__name__) 

45 

46PORT = 3000 

47 

48 

49class ConfigModel(ModelValidator): 

50 max_file_size: int 

51 osm_dashboards: bool 

52 site_url: Optional[str] 

53 ingress_whitelist_source_range: Optional[str] 

54 tls_secret_name: Optional[str] 

55 

56 @validator("max_file_size") 

57 def validate_max_file_size(cls, v): 

58 if v < 0: 

59 raise ValueError("value must be equal or greater than 0") 

60 return v 

61 

62 @validator("site_url") 

63 def validate_site_url(cls, v): 

64 if v: 

65 parsed = urlparse(v) 

66 if not parsed.scheme.startswith("http"): 

67 raise ValueError("value must start with http") 

68 return v 

69 

70 @validator("ingress_whitelist_source_range") 

71 def validate_ingress_whitelist_source_range(cls, v): 

72 if v: 

73 ip_network(v) 

74 return v 

75 

76 

77class GrafanaCharm(CharmedOsmBase): 

78 """GrafanaCharm Charm.""" 

79 

80 def __init__(self, *args) -> NoReturn: 

81 """Prometheus Charm constructor.""" 

82 super().__init__(*args, oci_image="image") 

83 

84 self.prometheus_client = PrometheusClient(self, "prometheus") 

85 self.framework.observe( 

86 self.on["prometheus"].relation_changed, self.configure_pod 

87 ) 

88 self.framework.observe( 

89 self.on["prometheus"].relation_broken, self.configure_pod 

90 ) 

91 

92 def _build_dashboard_files(self, config: ConfigModel): 

93 files_builder = FilesV3Builder() 

94 files_builder.add_file( 

95 "dashboard_osm.yaml", 

96 Path("files/default_dashboards.yaml").read_text(), 

97 ) 

98 if config.osm_dashboards: 

99 osm_dashboards_mapping = { 

100 "kafka_exporter_dashboard.json": "files/kafka_exporter_dashboard.json", 

101 "mongodb_exporter_dashboard.json": "files/mongodb_exporter_dashboard.json", 

102 "mysql_exporter_dashboard.json": "files/mysql_exporter_dashboard.json", 

103 "nodes_exporter_dashboard.json": "files/nodes_exporter_dashboard.json", 

104 "summary_dashboard.json": "files/summary_dashboard.json", 

105 } 

106 for file_name, path in osm_dashboards_mapping.items(): 

107 files_builder.add_file(file_name, Path(path).read_text()) 

108 return files_builder.build() 

109 

110 def _build_datasources_files(self): 

111 files_builder = FilesV3Builder() 

112 files_builder.add_file( 

113 "datasource_prometheus.yaml", 

114 Template(Path("files/default_datasources.yaml").read_text()).substitute( 

115 prometheus_host=self.prometheus_client.hostname, 

116 prometheus_port=self.prometheus_client.port, 

117 ), 

118 ) 

119 return files_builder.build() 

120 

121 def _check_missing_dependencies(self): 

122 missing_relations = [] 

123 

124 if self.prometheus_client.is_missing_data_in_app(): 

125 missing_relations.append("prometheus") 

126 

127 if missing_relations: 

128 raise RelationsMissing(missing_relations) 

129 

130 def build_pod_spec(self, image_info): 

131 # Validate config 

132 config = ConfigModel(**dict(self.config)) 

133 # Check relations 

134 self._check_missing_dependencies() 

135 # Create Builder for the PodSpec 

136 pod_spec_builder = PodSpecV3Builder() 

137 # Build Container 

138 container_builder = ContainerV3Builder(self.app.name, image_info) 

139 container_builder.add_port(name=self.app.name, port=PORT) 

140 container_builder.add_http_readiness_probe( 

141 "/api/health", 

142 PORT, 

143 initial_delay_seconds=10, 

144 period_seconds=10, 

145 timeout_seconds=5, 

146 failure_threshold=3, 

147 ) 

148 container_builder.add_http_liveness_probe( 

149 "/api/health", 

150 PORT, 

151 initial_delay_seconds=60, 

152 timeout_seconds=30, 

153 failure_threshold=10, 

154 ) 

155 container_builder.add_volume_config( 

156 "dashboards", 

157 "/etc/grafana/provisioning/dashboards/", 

158 self._build_dashboard_files(config), 

159 ) 

160 container_builder.add_volume_config( 

161 "datasources", 

162 "/etc/grafana/provisioning/datasources/", 

163 self._build_datasources_files(), 

164 ) 

165 container = container_builder.build() 

166 # Add container to pod spec 

167 pod_spec_builder.add_container(container) 

168 # Add ingress resources to pod spec if site url exists 

169 if config.site_url: 

170 parsed = urlparse(config.site_url) 

171 annotations = { 

172 "nginx.ingress.kubernetes.io/proxy-body-size": "{}".format( 

173 str(config.max_file_size) + "m" 

174 if config.max_file_size > 0 

175 else config.max_file_size 

176 ), 

177 } 

178 ingress_resource_builder = IngressResourceV3Builder( 

179 f"{self.app.name}-ingress", annotations 

180 ) 

181 

182 if config.ingress_whitelist_source_range: 

183 annotations[ 

184 "nginx.ingress.kubernetes.io/whitelist-source-range" 

185 ] = config.ingress_whitelist_source_range 

186 

187 if parsed.scheme == "https": 

188 ingress_resource_builder.add_tls( 

189 [parsed.hostname], config.tls_secret_name 

190 ) 

191 else: 

192 annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false" 

193 

194 ingress_resource_builder.add_rule(parsed.hostname, self.app.name, PORT) 

195 ingress_resource = ingress_resource_builder.build() 

196 pod_spec_builder.add_ingress_resource(ingress_resource) 

197 return pod_spec_builder.build() 

198 

199 

200if __name__ == "__main__": 

201 main(GrafanaCharm)