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 

23import logging 

24from ipaddress import ip_network 

25from typing import Any, Dict, List 

26from urllib.parse import urlparse 

27from pathlib import Path 

28from string import Template 

29 

30logger = logging.getLogger(__name__) 

31 

32 

33def _validate_max_file_size(max_file_size: int, site_url: str) -> bool: 

34 """Validate max_file_size. 

35 

36 Args: 

37 max_file_size (int): maximum file size allowed. 

38 site_url (str): endpoint url. 

39 

40 Returns: 

41 bool: True if valid, false otherwise. 

42 """ 

43 if not site_url: 

44 return True 

45 

46 parsed = urlparse(site_url) 

47 

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

49 return True 

50 

51 if max_file_size is None: 

52 return False 

53 

54 return max_file_size >= 0 

55 

56 

57def _validate_ip_network(network: str) -> bool: 

58 """Validate IP network. 

59 

60 Args: 

61 network (str): IP network range. 

62 

63 Returns: 

64 bool: True if valid, false otherwise. 

65 """ 

66 if not network: 

67 return True 

68 

69 try: 

70 ip_network(network) 

71 except ValueError: 

72 return False 

73 

74 return True 

75 

76 

77def _validate_data(config_data: Dict[str, Any], relation_data: Dict[str, Any]) -> bool: 

78 """Validates passed information. 

79 

80 Args: 

81 config_data (Dict[str, Any]): configuration information. 

82 relation_data (Dict[str, Any]): relation information 

83 

84 Raises: 

85 ValueError: when config and/or relation data is not valid. 

86 """ 

87 config_validators = { 

88 "site_url": lambda value, _: isinstance(value, str) 

89 if value is not None 

90 else True, 

91 "max_file_size": lambda value, values: _validate_max_file_size( 

92 value, values.get("site_url") 

93 ), 

94 "ingress_whitelist_source_range": lambda value, _: _validate_ip_network(value), 

95 "tls_secret_name": lambda value, _: isinstance(value, str) 

96 if value is not None 

97 else True, 

98 } 

99 relation_validators = { 

100 "prometheus_hostname": lambda value, _: ( 

101 isinstance(value, str) and len(value) > 0 

102 ), 

103 "prometheus_port": lambda value, _: ( 

104 isinstance(value, str) and len(value) > 0 and int(value) > 0 

105 ), 

106 } 

107 problems = [] 

108 

109 for key, validator in config_validators.items(): 

110 valid = validator(config_data.get(key), config_data) 

111 

112 if not valid: 

113 problems.append(key) 

114 

115 for key, validator in relation_validators.items(): 

116 valid = validator(relation_data.get(key), relation_data) 

117 

118 if not valid: 

119 problems.append(key) 

120 

121 if len(problems) > 0: 

122 logger.debug(relation_data) 

123 raise ValueError("Errors found in: {}".format(", ".join(problems))) 

124 

125 return True 

126 

127 

128def _make_pod_ports(port: int) -> List[Dict[str, Any]]: 

129 """Generate pod ports details. 

130 

131 Args: 

132 port (int): port to expose. 

133 

134 Returns: 

135 List[Dict[str, Any]]: pod port details. 

136 """ 

137 return [{"name": "grafana", "containerPort": port, "protocol": "TCP"}] 

138 

139 

140def _make_pod_envconfig( 

141 config: Dict[str, Any], relation_state: Dict[str, Any] 

142) -> Dict[str, Any]: 

143 """Generate pod environment configuration. 

144 

145 Args: 

146 config (Dict[str, Any]): configuration information. 

147 relation_state (Dict[str, Any]): relation state information. 

148 

149 Returns: 

150 Dict[str, Any]: pod environment configuration. 

151 """ 

152 envconfig = {} 

153 

154 return envconfig 

155 

156 

157def _make_pod_ingress_resources( 

158 config: Dict[str, Any], app_name: str, port: int 

159) -> List[Dict[str, Any]]: 

160 """Generate pod ingress resources. 

161 

162 Args: 

163 config (Dict[str, Any]): configuration information. 

164 app_name (str): application name. 

165 port (int): port to expose. 

166 

167 Returns: 

168 List[Dict[str, Any]]: pod ingress resources. 

169 """ 

170 site_url = config.get("site_url") 

171 

172 if not site_url: 

173 return 

174 

175 parsed = urlparse(site_url) 

176 

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

178 return 

179 

180 max_file_size = config["max_file_size"] 

181 ingress_whitelist_source_range = config["ingress_whitelist_source_range"] 

182 

183 annotations = { 

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

185 str(max_file_size) + "m" if max_file_size > 0 else max_file_size 

186 ), 

187 } 

188 

189 if ingress_whitelist_source_range: 

190 annotations[ 

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

192 ] = ingress_whitelist_source_range 

193 

194 ingress_spec_tls = None 

195 

196 if parsed.scheme == "https": 

197 ingress_spec_tls = [{"hosts": [parsed.hostname]}] 

198 tls_secret_name = config["tls_secret_name"] 

199 if tls_secret_name: 

200 ingress_spec_tls[0]["secretName"] = tls_secret_name 

201 else: 

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

203 

204 ingress = { 

205 "name": "{}-ingress".format(app_name), 

206 "annotations": annotations, 

207 "spec": { 

208 "rules": [ 

209 { 

210 "host": parsed.hostname, 

211 "http": { 

212 "paths": [ 

213 { 

214 "path": "/", 

215 "backend": { 

216 "serviceName": app_name, 

217 "servicePort": port, 

218 }, 

219 } 

220 ] 

221 }, 

222 } 

223 ] 

224 }, 

225 } 

226 if ingress_spec_tls: 

227 ingress["spec"]["tls"] = ingress_spec_tls 

228 

229 return [ingress] 

230 

231 

232def _make_pod_files( 

233 config: Dict[str, Any], relation: Dict[str, Any] 

234) -> List[Dict[str, Any]]: 

235 """Generating ConfigMap information 

236 

237 Args: 

238 config (Dict[str, Any]): configuration information. 

239 relation (Dict[str, Any]): relation information. 

240 

241 Returns: 

242 List[Dict[str, Any]]: ConfigMap information. 

243 """ 

244 template_data = {**config, **relation} 

245 dashboards = [] 

246 

247 if config.get("osm_dashboards", False): 

248 dashboards.extend( 

249 [ 

250 { 

251 "path": "kafka_exporter_dashboard.json", 

252 "content": Path("files/kafka_exporter_dashboard.json").read_text(), 

253 }, 

254 { 

255 "path": "mongodb_exporter_dashboard.json", 

256 "content": Path( 

257 "files/mongodb_exporter_dashboard.json" 

258 ).read_text(), 

259 }, 

260 { 

261 "path": "mysql_exporter_dashboard.json", 

262 "content": Path("files/mysql_exporter_dashboard.json").read_text(), 

263 }, 

264 { 

265 "path": "nodes_exporter_dashboard.json", 

266 "content": Path("files/nodes_exporter_dashboard.json").read_text(), 

267 }, 

268 { 

269 "path": "summary_dashboard.json", 

270 "content": Path("files/summary_dashboard.json").read_text(), 

271 }, 

272 ] 

273 ) 

274 

275 dashboards.append( 

276 { 

277 "path": "dashboard_osm.yaml", 

278 "content": Path("files/default_dashboards.yaml").read_text(), 

279 } 

280 ) 

281 

282 files = [ 

283 { 

284 "name": "dashboards", 

285 "mountPath": "/etc/grafana/provisioning/dashboards/", 

286 "files": dashboards, 

287 }, 

288 { 

289 "name": "datasources", 

290 "mountPath": "/etc/grafana/provisioning/datasources/", 

291 "files": [ 

292 { 

293 "path": "datasource_prometheus.yaml", 

294 "content": Template( 

295 Path("files/default_dashboards.yaml").read_text() 

296 ).substitute(template_data), 

297 } 

298 ], 

299 }, 

300 ] 

301 

302 return files 

303 

304 

305def _make_readiness_probe(port: int) -> Dict[str, Any]: 

306 """Generate readiness probe. 

307 

308 Args: 

309 port (int): service port. 

310 

311 Returns: 

312 Dict[str, Any]: readiness probe. 

313 """ 

314 return { 

315 "httpGet": { 

316 "path": "/api/health", 

317 "port": port, 

318 }, 

319 "initialDelaySeconds": 10, 

320 "periodSeconds": 10, 

321 "timeoutSeconds": 5, 

322 "successThreshold": 1, 

323 "failureThreshold": 3, 

324 } 

325 

326 

327def _make_liveness_probe(port: int) -> Dict[str, Any]: 

328 """Generate liveness probe. 

329 

330 Args: 

331 port (int): service port. 

332 

333 Returns: 

334 Dict[str, Any]: liveness probe. 

335 """ 

336 return { 

337 "httpGet": { 

338 "path": "/api/health", 

339 "port": port, 

340 }, 

341 "initialDelaySeconds": 60, 

342 "timeoutSeconds": 30, 

343 "failureThreshold": 10, 

344 } 

345 

346 

347def make_pod_spec( 

348 image_info: Dict[str, str], 

349 config: Dict[str, Any], 

350 relation_state: Dict[str, Any], 

351 app_name: str = "grafana", 

352 port: int = 3000, 

353) -> Dict[str, Any]: 

354 """Generate the pod spec information. 

355 

356 Args: 

357 image_info (Dict[str, str]): Object provided by 

358 OCIImageResource("image").fetch(). 

359 config (Dict[str, Any]): Configuration information. 

360 relation_state (Dict[str, Any]): Relation state information. 

361 app_name (str, optional): Application name. Defaults to "ro". 

362 port (int, optional): Port for the container. Defaults to 9090. 

363 

364 Returns: 

365 Dict[str, Any]: Pod spec dictionary for the charm. 

366 """ 

367 if not image_info: 

368 return None 

369 

370 _validate_data(config, relation_state) 

371 

372 ports = _make_pod_ports(port) 

373 env_config = _make_pod_envconfig(config, relation_state) 

374 files = _make_pod_files(config, relation_state) 

375 readiness_probe = _make_readiness_probe(port) 

376 liveness_probe = _make_liveness_probe(port) 

377 ingress_resources = _make_pod_ingress_resources(config, app_name, port) 

378 

379 return { 

380 "version": 3, 

381 "containers": [ 

382 { 

383 "name": app_name, 

384 "imageDetails": image_info, 

385 "imagePullPolicy": "Always", 

386 "ports": ports, 

387 "envConfig": env_config, 

388 "volumeConfig": files, 

389 "kubernetes": { 

390 "readinessProbe": readiness_probe, 

391 "livenessProbe": liveness_probe, 

392 }, 

393 } 

394 ], 

395 "kubernetesResources": { 

396 "ingressResources": ingress_resources or [], 

397 }, 

398 }