bf301f32c74507856fb55ea83ffe83c0af4be457
[osm/devops.git] / installers / charm / ng-ui / src / charm.py
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
26 from ipaddress import ip_network
27 import logging
28 from pathlib import Path
29 from string import Template
30 from typing import NoReturn, Optional
31 from urllib.parse import urlparse
32
33 from ops.main import main
34 from opslib.osm.charm import CharmedOsmBase, RelationsMissing
35 from opslib.osm.interfaces.http import HttpClient
36 from opslib.osm.pod import (
37 ContainerV3Builder,
38 FilesV3Builder,
39 IngressResourceV3Builder,
40 PodSpecV3Builder,
41 )
42 from opslib.osm.validator import ModelValidator, validator
43
44
45 logger = logging.getLogger(__name__)
46
47
48 class 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
83 class 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
176 if __name__ == "__main__":
177 main(NgUiCharm)