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 2020 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,E0611 

24 

25 

26import logging 

27from pydantic import ( 

28 BaseModel, 

29 conint, 

30 IPvAnyNetwork, 

31 PositiveInt, 

32 validator, 

33) 

34from typing import Any, Dict, List, Optional 

35from urllib.parse import urlparse 

36from pathlib import Path 

37from string import Template 

38 

39logger = logging.getLogger(__name__) 

40 

41 

42class ConfigData(BaseModel): 

43 """Configuration data model.""" 

44 

45 port: PositiveInt 

46 site_url: Optional[str] 

47 max_file_size: Optional[conint(ge=0)] 

48 ingress_whitelist_source_range: Optional[IPvAnyNetwork] 

49 tls_secret_name: Optional[str] 

50 

51 @validator("max_file_size", pre=True, always=True) 

52 def validate_max_file_size(cls, value, values, **kwargs): 

53 site_url = values.get("site_url") 

54 

55 if not site_url: 

56 return value 

57 

58 parsed = urlparse(site_url) 

59 

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

61 return value 

62 

63 if value is None: 

64 raise ValueError("max_file_size needs to be defined if site_url is defined") 

65 

66 return value 

67 

68 @validator("ingress_whitelist_source_range", pre=True, always=True) 

69 def validate_ingress_whitelist_source_range(cls, value, values, **kwargs): 

70 if not value: 

71 return None 

72 

73 return value 

74 

75 

76class RelationData(BaseModel): 

77 """Relation data model.""" 

78 

79 nbi_host: str 

80 nbi_port: PositiveInt 

81 

82 

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

84 """Generate pod ports details. 

85 

86 Args: 

87 port (int): Port to expose. 

88 

89 Returns: 

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

91 """ 

92 return [ 

93 {"name": "http", "containerPort": port, "protocol": "TCP"}, 

94 ] 

95 

96 

97def _make_pod_ingress_resources( 

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

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

100 """Generate pod ingress resources. 

101 

102 Args: 

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

104 app_name (str): application name. 

105 port (int): port to expose. 

106 

107 Returns: 

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

109 """ 

110 site_url = config.get("site_url") 

111 

112 if not site_url: 

113 return 

114 

115 parsed = urlparse(site_url) 

116 

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

118 return 

119 

120 max_file_size = config["max_file_size"] 

121 ingress_whitelist_source_range = config["ingress_whitelist_source_range"] 

122 

123 annotations = { 

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

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

126 ), 

127 } 

128 

129 if ingress_whitelist_source_range: 

130 annotations[ 

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

132 ] = ingress_whitelist_source_range 

133 

134 ingress_spec_tls = None 

135 

136 if parsed.scheme == "https": 

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

138 tls_secret_name = config["tls_secret_name"] 

139 if tls_secret_name: 

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

141 else: 

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

143 

144 ingress = { 

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

146 "annotations": annotations, 

147 "spec": { 

148 "rules": [ 

149 { 

150 "host": parsed.hostname, 

151 "http": { 

152 "paths": [ 

153 { 

154 "path": "/", 

155 "backend": { 

156 "serviceName": app_name, 

157 "servicePort": port, 

158 }, 

159 } 

160 ] 

161 }, 

162 } 

163 ] 

164 }, 

165 } 

166 if ingress_spec_tls: 

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

168 

169 return [ingress] 

170 

171 

172def _make_startup_probe() -> Dict[str, Any]: 

173 """Generate startup probe. 

174 

175 Returns: 

176 Dict[str, Any]: startup probe. 

177 """ 

178 return { 

179 "exec": {"command": ["/usr/bin/pgrep python3"]}, 

180 "initialDelaySeconds": 60, 

181 "timeoutSeconds": 5, 

182 } 

183 

184 

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

186 """Generate readiness probe. 

187 

188 Args: 

189 port (int): [description] 

190 

191 Returns: 

192 Dict[str, Any]: readiness probe. 

193 """ 

194 return { 

195 "tcpSocket": { 

196 "port": port, 

197 }, 

198 "initialDelaySeconds": 45, 

199 "timeoutSeconds": 5, 

200 } 

201 

202 

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

204 """Generate liveness probe. 

205 

206 Args: 

207 port (int): [description] 

208 

209 Returns: 

210 Dict[str, Any]: liveness probe. 

211 """ 

212 return { 

213 "tcpSocket": { 

214 "port": port, 

215 }, 

216 "initialDelaySeconds": 45, 

217 "timeoutSeconds": 5, 

218 } 

219 

220 

221def _make_pod_volume_config( 

222 config: Dict[str, Any], 

223 relation_state: Dict[str, Any], 

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

225 """Generate volume config with files. 

226 

227 Args: 

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

229 

230 Returns: 

231 Dict[str, Any]: volume config. 

232 """ 

233 template_data = {**config, **relation_state} 

234 template_data["max_file_size"] = f'{template_data["max_file_size"]}M' 

235 return [ 

236 { 

237 "name": "configuration", 

238 "mountPath": "/etc/nginx/sites-available/", 

239 "files": [ 

240 { 

241 "path": "default", 

242 "content": Template(Path("files/default").read_text()).substitute( 

243 template_data 

244 ), 

245 } 

246 ], 

247 } 

248 ] 

249 

250 

251def make_pod_spec( 

252 image_info: Dict[str, str], 

253 config: Dict[str, Any], 

254 relation_state: Dict[str, Any], 

255 app_name: str = "ng-ui", 

256) -> Dict[str, Any]: 

257 """Generate the pod spec information. 

258 

259 Args: 

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

261 OCIImageResource("image").fetch(). 

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

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

264 app_name (str, optional): Application name. Defaults to "ng-ui". 

265 port (int, optional): Port for the container. Defaults to 80. 

266 

267 Returns: 

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

269 """ 

270 if not image_info: 

271 return None 

272 

273 ConfigData(**(config)) 

274 RelationData(**(relation_state)) 

275 

276 ports = _make_pod_ports(config["port"]) 

277 ingress_resources = _make_pod_ingress_resources(config, app_name, config["port"]) 

278 kubernetes = { 

279 # "startupProbe": _make_startup_probe(), 

280 "readinessProbe": _make_readiness_probe(config["port"]), 

281 "livenessProbe": _make_liveness_probe(config["port"]), 

282 } 

283 volume_config = _make_pod_volume_config(config, relation_state) 

284 return { 

285 "version": 3, 

286 "containers": [ 

287 { 

288 "name": app_name, 

289 "imageDetails": image_info, 

290 "imagePullPolicy": "Always", 

291 "ports": ports, 

292 "kubernetes": kubernetes, 

293 "volumeConfig": volume_config, 

294 } 

295 ], 

296 "kubernetesResources": { 

297 "ingressResources": ingress_resources or [], 

298 }, 

299 }