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 

25 

26from ipaddress import ip_network 

27import logging 

28from pathlib import Path 

29from string import Template 

30from typing import NoReturn, Optional 

31from urllib.parse import urlparse 

32 

33from ops.main import main 

34from opslib.osm.charm import CharmedOsmBase, RelationsMissing 

35from opslib.osm.interfaces.http import HttpClient 

36from opslib.osm.pod import ( 

37 ContainerV3Builder, 

38 FilesV3Builder, 

39 IngressResourceV3Builder, 

40 PodSpecV3Builder, 

41) 

42from opslib.osm.validator import ModelValidator, validator 

43 

44 

45logger = logging.getLogger(__name__) 

46 

47 

48class ConfigModel(ModelValidator): 

49 port: int 

50 server_name: str 

51 max_file_size: int 

52 site_url: Optional[str] 

53 ingress_whitelist_source_range: Optional[str] 

54 tls_secret_name: Optional[str] 

55 

56 @validator("port") 

57 def validate_port(cls, v): 

58 if v <= 0: 

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

60 return v 

61 

62 @validator("max_file_size") 

63 def validate_max_file_size(cls, v): 

64 if v < 0: 

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

66 return v 

67 

68 @validator("site_url") 

69 def validate_site_url(cls, v): 

70 if v: 

71 parsed = urlparse(v) 

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

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

74 return v 

75 

76 @validator("ingress_whitelist_source_range") 

77 def validate_ingress_whitelist_source_range(cls, v): 

78 if v: 

79 ip_network(v) 

80 return v 

81 

82 

83class NgUiCharm(CharmedOsmBase): 

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

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

86 

87 self.nbi_client = HttpClient(self, "nbi") 

88 self.framework.observe(self.on["nbi"].relation_changed, self.configure_pod) 

89 self.framework.observe(self.on["nbi"].relation_broken, self.configure_pod) 

90 

91 def _check_missing_dependencies(self, config: ConfigModel): 

92 missing_relations = [] 

93 

94 if self.nbi_client.is_missing_data_in_app(): 

95 missing_relations.append("nbi") 

96 

97 if missing_relations: 

98 raise RelationsMissing(missing_relations) 

99 

100 def _build_files(self, config: ConfigModel): 

101 files_builder = FilesV3Builder() 

102 files_builder.add_file( 

103 "default", 

104 Template(Path("files/default").read_text()).substitute( 

105 port=config.port, 

106 server_name=config.server_name, 

107 max_file_size=config.max_file_size, 

108 nbi_host=self.nbi_client.host, 

109 nbi_port=self.nbi_client.port, 

110 ), 

111 ) 

112 return files_builder.build() 

113 

114 def build_pod_spec(self, image_info): 

115 # Validate config 

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

117 # Check relations 

118 self._check_missing_dependencies(config) 

119 # Create Builder for the PodSpec 

120 pod_spec_builder = PodSpecV3Builder() 

121 # Build Container 

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

123 container_builder.add_port(name=self.app.name, port=config.port) 

124 container = container_builder.build() 

125 container_builder.add_tcpsocket_readiness_probe( 

126 config.port, 

127 initial_delay_seconds=45, 

128 timeout_seconds=5, 

129 ) 

130 container_builder.add_tcpsocket_liveness_probe( 

131 config.port, 

132 initial_delay_seconds=45, 

133 timeout_seconds=15, 

134 ) 

135 container_builder.add_volume_config( 

136 "configuration", 

137 "/etc/nginx/sites-available/", 

138 self._build_files(config), 

139 ) 

140 # Add container to pod spec 

141 pod_spec_builder.add_container(container) 

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

143 if config.site_url: 

144 parsed = urlparse(config.site_url) 

145 annotations = { 

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

147 str(config.max_file_size) + "m" 

148 if config.max_file_size > 0 

149 else config.max_file_size 

150 ), 

151 } 

152 ingress_resource_builder = IngressResourceV3Builder( 

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

154 ) 

155 

156 if config.ingress_whitelist_source_range: 

157 annotations[ 

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

159 ] = config.ingress_whitelist_source_range 

160 

161 if parsed.scheme == "https": 

162 ingress_resource_builder.add_tls( 

163 [parsed.hostname], config.tls_secret_name 

164 ) 

165 else: 

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

167 

168 ingress_resource_builder.add_rule( 

169 parsed.hostname, self.app.name, config.port 

170 ) 

171 ingress_resource = ingress_resource_builder.build() 

172 pod_spec_builder.add_ingress_resource(ingress_resource) 

173 return pod_spec_builder.build() 

174 

175 

176if __name__ == "__main__": 

177 main(NgUiCharm)