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 

23from ipaddress import ip_network 

24import logging 

25from typing import Any, Dict, List 

26from urllib.parse import urlparse 

27 

28logger = logging.getLogger(__name__) 

29 

30 

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

32 """Validate max_file_size. 

33 

34 Args: 

35 max_file_size (int): maximum file size allowed. 

36 site_url (str): endpoint url. 

37 

38 Returns: 

39 bool: True if valid, false otherwise. 

40 """ 

41 if not site_url: 

42 return True 

43 

44 parsed = urlparse(site_url) 

45 

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

47 return True 

48 

49 if max_file_size is None: 

50 return False 

51 

52 return max_file_size >= 0 

53 

54 

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

56 """Validate IP network. 

57 

58 Args: 

59 network (str): IP network range. 

60 

61 Returns: 

62 bool: True if valid, false otherwise. 

63 """ 

64 if not network: 

65 return True 

66 

67 try: 

68 ip_network(network) 

69 except ValueError: 

70 return False 

71 

72 return True 

73 

74 

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

76 """Validates passed information. 

77 

78 Args: 

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

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

81 

82 Raises: 

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

84 """ 

85 config_validators = { 

86 "web_subpath": lambda value, _: isinstance(value, str) and len(value) > 0, 

87 "default_target": lambda value, _: isinstance(value, str), 

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 "enable_web_admin_api": lambda value, _: isinstance(value, bool), 

99 } 

100 relation_validators = {} 

101 problems = [] 

102 

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

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

105 

106 if not valid: 

107 problems.append(key) 

108 

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

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

111 

112 if not valid: 

113 problems.append(key) 

114 

115 if len(problems) > 0: 

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

117 

118 return True 

119 

120 

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

122 """Generate pod ports details. 

123 

124 Args: 

125 port (int): port to expose. 

126 

127 Returns: 

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

129 """ 

130 return [{"name": "prometheus", "containerPort": port, "protocol": "TCP"}] 

131 

132 

133def _make_pod_envconfig( 

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

135) -> Dict[str, Any]: 

136 """Generate pod environment configuration. 

137 

138 Args: 

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

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

141 

142 Returns: 

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

144 """ 

145 envconfig = {} 

146 

147 return envconfig 

148 

149 

150def _make_pod_ingress_resources( 

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

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

153 """Generate pod ingress resources. 

154 

155 Args: 

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

157 app_name (str): application name. 

158 port (int): port to expose. 

159 

160 Returns: 

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

162 """ 

163 site_url = config.get("site_url") 

164 

165 if not site_url: 

166 return 

167 

168 parsed = urlparse(site_url) 

169 

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

171 return 

172 

173 max_file_size = config["max_file_size"] 

174 ingress_whitelist_source_range = config["ingress_whitelist_source_range"] 

175 

176 annotations = { 

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

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

179 ), 

180 } 

181 

182 if ingress_whitelist_source_range: 

183 annotations[ 

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

185 ] = ingress_whitelist_source_range 

186 

187 ingress_spec_tls = None 

188 

189 if parsed.scheme == "https": 

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

191 tls_secret_name = config["tls_secret_name"] 

192 if tls_secret_name: 

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

194 else: 

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

196 

197 ingress = { 

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

199 "annotations": annotations, 

200 "spec": { 

201 "rules": [ 

202 { 

203 "host": parsed.hostname, 

204 "http": { 

205 "paths": [ 

206 { 

207 "path": "/", 

208 "backend": { 

209 "serviceName": app_name, 

210 "servicePort": port, 

211 }, 

212 } 

213 ] 

214 }, 

215 } 

216 ] 

217 }, 

218 } 

219 if ingress_spec_tls: 

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

221 

222 return [ingress] 

223 

224 

225def _make_pod_files(config: Dict[str, Any]) -> List[Dict[str, Any]]: 

226 """Generating ConfigMap information 

227 

228 Args: 

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

230 

231 Returns: 

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

233 """ 

234 files = [ 

235 { 

236 "name": "config", 

237 "mountPath": "/etc/prometheus", 

238 "files": [ 

239 { 

240 "path": "prometheus.yml", 

241 "content": ( 

242 "global:\n" 

243 " scrape_interval: 15s\n" 

244 " evaluation_interval: 15s\n" 

245 "alerting:\n" 

246 " alertmanagers:\n" 

247 " - static_configs:\n" 

248 " - targets:\n" 

249 "rule_files:\n" 

250 "scrape_configs:\n" 

251 " - job_name: 'prometheus'\n" 

252 " static_configs:\n" 

253 " - targets: [{}]\n".format(config["default_target"]) 

254 ), 

255 } 

256 ], 

257 } 

258 ] 

259 

260 return files 

261 

262 

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

264 """Generate readiness probe. 

265 

266 Args: 

267 port (int): service port. 

268 

269 Returns: 

270 Dict[str, Any]: readiness probe. 

271 """ 

272 return { 

273 "httpGet": { 

274 "path": "/-/ready", 

275 "port": port, 

276 }, 

277 "initialDelaySeconds": 10, 

278 "timeoutSeconds": 30, 

279 } 

280 

281 

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

283 """Generate liveness probe. 

284 

285 Args: 

286 port (int): service port. 

287 

288 Returns: 

289 Dict[str, Any]: liveness probe. 

290 """ 

291 return { 

292 "httpGet": { 

293 "path": "/-/healthy", 

294 "port": port, 

295 }, 

296 "initialDelaySeconds": 30, 

297 "periodSeconds": 30, 

298 } 

299 

300 

301def _make_pod_command(config: Dict[str, Any], port: int) -> List[str]: 

302 """Generate the startup command. 

303 

304 Args: 

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

306 port (int): port. 

307 

308 Returns: 

309 List[str]: command to startup the process. 

310 """ 

311 command = [ 

312 "/bin/prometheus", 

313 "--config.file=/etc/prometheus/prometheus.yml", 

314 "--storage.tsdb.path=/prometheus", 

315 "--web.console.libraries=/usr/share/prometheus/console_libraries", 

316 "--web.console.templates=/usr/share/prometheus/consoles", 

317 "--web.route-prefix={}".format(config.get("web_subpath")), 

318 "--web.external-url=http://localhost:{}{}".format( 

319 port, config.get("web_subpath") 

320 ), 

321 ] 

322 if config.get("enable_web_admin_api"): 

323 command.append("--web.enable-admin-api") 

324 return command 

325 

326 

327def make_pod_spec( 

328 image_info: Dict[str, str], 

329 config: Dict[str, Any], 

330 relation_state: Dict[str, Any], 

331 app_name: str = "prometheus", 

332 port: int = 9090, 

333) -> Dict[str, Any]: 

334 """Generate the pod spec information. 

335 

336 Args: 

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

338 OCIImageResource("image").fetch(). 

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

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

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

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

343 

344 Returns: 

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

346 """ 

347 if not image_info: 

348 return None 

349 

350 _validate_data(config, relation_state) 

351 

352 ports = _make_pod_ports(port) 

353 env_config = _make_pod_envconfig(config, relation_state) 

354 files = _make_pod_files(config) 

355 readiness_probe = _make_readiness_probe(port) 

356 liveness_probe = _make_liveness_probe(port) 

357 ingress_resources = _make_pod_ingress_resources(config, app_name, port) 

358 command = _make_pod_command(config, port) 

359 

360 return { 

361 "version": 3, 

362 "containers": [ 

363 { 

364 "name": app_name, 

365 "imageDetails": image_info, 

366 "imagePullPolicy": "Always", 

367 "ports": ports, 

368 "envConfig": env_config, 

369 "volumeConfig": files, 

370 "command": command, 

371 "kubernetes": { 

372 "readinessProbe": readiness_probe, 

373 "livenessProbe": liveness_probe, 

374 }, 

375 } 

376 ], 

377 "kubernetesResources": { 

378 "ingressResources": ingress_resources or [], 

379 }, 

380 }