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 typing import NoReturn, Optional 

28from urllib.parse import urlparse 

29 

30from oci_image import OCIImageResource 

31from ops.framework import EventBase 

32from ops.main import main 

33from opslib.osm.charm import CharmedOsmBase 

34from opslib.osm.interfaces.prometheus import PrometheusServer 

35from opslib.osm.pod import ( 

36 ContainerV3Builder, 

37 FilesV3Builder, 

38 IngressResourceV3Builder, 

39 PodSpecV3Builder, 

40) 

41from opslib.osm.validator import ( 

42 ModelValidator, 

43 validator, 

44) 

45import requests 

46 

47 

48logger = logging.getLogger(__name__) 

49 

50PORT = 9090 

51 

52 

53class ConfigModel(ModelValidator): 

54 web_subpath: str 

55 default_target: str 

56 max_file_size: int 

57 site_url: Optional[str] 

58 ingress_whitelist_source_range: Optional[str] 

59 tls_secret_name: Optional[str] 

60 enable_web_admin_api: bool 

61 

62 @validator("web_subpath") 

63 def validate_web_subpath(cls, v): 

64 if len(v) < 1: 

65 raise ValueError("web-subpath must be a non-empty string") 

66 return v 

67 

68 @validator("max_file_size") 

69 def validate_max_file_size(cls, v): 

70 if v < 0: 

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

72 return v 

73 

74 @validator("site_url") 

75 def validate_site_url(cls, v): 

76 if v: 

77 parsed = urlparse(v) 

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

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

80 return v 

81 

82 @validator("ingress_whitelist_source_range") 

83 def validate_ingress_whitelist_source_range(cls, v): 

84 if v: 

85 ip_network(v) 

86 return v 

87 

88 

89class PrometheusCharm(CharmedOsmBase): 

90 

91 """Prometheus Charm.""" 

92 

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

94 """Prometheus Charm constructor.""" 

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

96 

97 # Registering provided relation events 

98 self.prometheus = PrometheusServer(self, "prometheus") 

99 self.framework.observe( 

100 self.on.prometheus_relation_joined, # pylint: disable=E1101 

101 self._publish_prometheus_info, 

102 ) 

103 

104 # Registering actions 

105 self.framework.observe( 

106 self.on.backup_action, # pylint: disable=E1101 

107 self._on_backup_action, 

108 ) 

109 

110 def _publish_prometheus_info(self, event: EventBase) -> NoReturn: 

111 self.prometheus.publish_info(self.app.name, PORT) 

112 

113 def _on_backup_action(self, event: EventBase) -> NoReturn: 

114 url = f"http://{self.model.app.name}:{PORT}/api/v2/admin/tsdb/snapshot" 

115 result = requests.post(url) 

116 

117 if result.status_code == 200: 

118 event.set_results({"backup-name": result.json()["name"]}) 

119 else: 

120 event.fail(f"status-code: {result.status_code}, result: {result.json()}") 

121 

122 def _build_files(self, config: ConfigModel): 

123 files_builder = FilesV3Builder() 

124 files_builder.add_file( 

125 "prometheus.yml", 

126 ( 

127 "global:\n" 

128 " scrape_interval: 15s\n" 

129 " evaluation_interval: 15s\n" 

130 "alerting:\n" 

131 " alertmanagers:\n" 

132 " - static_configs:\n" 

133 " - targets:\n" 

134 "rule_files:\n" 

135 "scrape_configs:\n" 

136 " - job_name: 'prometheus'\n" 

137 " static_configs:\n" 

138 f" - targets: [{config.default_target}]\n" 

139 ), 

140 ) 

141 return files_builder.build() 

142 

143 def build_pod_spec(self, image_info): 

144 # Validate config 

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

146 # Create Builder for the PodSpec 

147 pod_spec_builder = PodSpecV3Builder() 

148 

149 # Build Backup Container 

150 backup_image = OCIImageResource(self, "backup-image") 

151 backup_image_info = backup_image.fetch() 

152 backup_container_builder = ContainerV3Builder("prom-backup", backup_image_info) 

153 backup_container = backup_container_builder.build() 

154 # Add backup container to pod spec 

155 pod_spec_builder.add_container(backup_container) 

156 

157 # Build Container 

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

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

160 container_builder.add_http_readiness_probe( 

161 "/-/ready", 

162 PORT, 

163 initial_delay_seconds=10, 

164 timeout_seconds=30, 

165 ) 

166 container_builder.add_http_liveness_probe( 

167 "/-/healthy", 

168 PORT, 

169 initial_delay_seconds=30, 

170 period_seconds=30, 

171 ) 

172 command = [ 

173 "/bin/prometheus", 

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

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

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

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

178 f"--web.route-prefix={config.web_subpath}", 

179 f"--web.external-url=http://localhost:{PORT}{config.web_subpath}", 

180 ] 

181 if config.enable_web_admin_api: 

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

183 container_builder.add_command(command) 

184 container_builder.add_volume_config( 

185 "config", "/etc/prometheus", self._build_files(config) 

186 ) 

187 container = container_builder.build() 

188 # Add container to pod spec 

189 pod_spec_builder.add_container(container) 

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

191 if config.site_url: 

192 parsed = urlparse(config.site_url) 

193 annotations = { 

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

195 str(config.max_file_size) + "m" 

196 if config.max_file_size > 0 

197 else config.max_file_size 

198 ), 

199 } 

200 ingress_resource_builder = IngressResourceV3Builder( 

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

202 ) 

203 

204 if config.ingress_whitelist_source_range: 

205 annotations[ 

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

207 ] = config.ingress_whitelist_source_range 

208 

209 if parsed.scheme == "https": 

210 ingress_resource_builder.add_tls( 

211 [parsed.hostname], config.tls_secret_name 

212 ) 

213 else: 

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

215 

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

217 ingress_resource = ingress_resource_builder.build() 

218 pod_spec_builder.add_ingress_resource(ingress_resource) 

219 return pod_spec_builder.build() 

220 

221 

222if __name__ == "__main__": 

223 main(PrometheusCharm)