5388466e8a47ec783ca9e4fbd661dd2e4e0f1883
[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 cluster_issuer: Optional[str]
54 ingress_class: Optional[str]
55 ingress_whitelist_source_range: Optional[str]
56 tls_secret_name: Optional[str]
57
58 @validator("port")
59 def validate_port(cls, v):
60 if v <= 0:
61 raise ValueError("value must be greater than 0")
62 return v
63
64 @validator("max_file_size")
65 def validate_max_file_size(cls, v):
66 if v < 0:
67 raise ValueError("value must be equal or greater than 0")
68 return v
69
70 @validator("site_url")
71 def validate_site_url(cls, v):
72 if v:
73 parsed = urlparse(v)
74 if not parsed.scheme.startswith("http"):
75 raise ValueError("value must start with http")
76 return v
77
78 @validator("ingress_whitelist_source_range")
79 def validate_ingress_whitelist_source_range(cls, v):
80 if v:
81 ip_network(v)
82 return v
83
84
85 class NgUiCharm(CharmedOsmBase):
86 def __init__(self, *args) -> NoReturn:
87 super().__init__(*args, oci_image="image")
88
89 self.nbi_client = HttpClient(self, "nbi")
90 self.framework.observe(self.on["nbi"].relation_changed, self.configure_pod)
91 self.framework.observe(self.on["nbi"].relation_broken, self.configure_pod)
92
93 def _check_missing_dependencies(self, config: ConfigModel):
94 missing_relations = []
95
96 if self.nbi_client.is_missing_data_in_app():
97 missing_relations.append("nbi")
98
99 if missing_relations:
100 raise RelationsMissing(missing_relations)
101
102 def _build_files(self, config: ConfigModel):
103 files_builder = FilesV3Builder()
104 files_builder.add_file(
105 "default",
106 Template(Path("files/default").read_text()).substitute(
107 port=config.port,
108 server_name=config.server_name,
109 max_file_size=config.max_file_size,
110 nbi_host=self.nbi_client.host,
111 nbi_port=self.nbi_client.port,
112 ),
113 )
114 return files_builder.build()
115
116 def build_pod_spec(self, image_info):
117 # Validate config
118 config = ConfigModel(**dict(self.config))
119 # Check relations
120 self._check_missing_dependencies(config)
121 # Create Builder for the PodSpec
122 pod_spec_builder = PodSpecV3Builder()
123 # Build Container
124 container_builder = ContainerV3Builder(self.app.name, image_info)
125 container_builder.add_port(name=self.app.name, port=config.port)
126 container = container_builder.build()
127 container_builder.add_tcpsocket_readiness_probe(
128 config.port,
129 initial_delay_seconds=45,
130 timeout_seconds=5,
131 )
132 container_builder.add_tcpsocket_liveness_probe(
133 config.port,
134 initial_delay_seconds=45,
135 timeout_seconds=15,
136 )
137 container_builder.add_volume_config(
138 "configuration",
139 "/etc/nginx/sites-available/",
140 self._build_files(config),
141 )
142 # Add container to pod spec
143 pod_spec_builder.add_container(container)
144 # Add ingress resources to pod spec if site url exists
145 if config.site_url:
146 parsed = urlparse(config.site_url)
147 annotations = {
148 "nginx.ingress.kubernetes.io/proxy-body-size": "{}".format(
149 str(config.max_file_size) + "m"
150 if config.max_file_size > 0
151 else config.max_file_size
152 )
153 }
154 if config.ingress_class:
155 annotations["kubernetes.io/ingress.class"] = config.ingress_class
156 ingress_resource_builder = IngressResourceV3Builder(
157 f"{self.app.name}-ingress", annotations
158 )
159
160 if config.ingress_whitelist_source_range:
161 annotations[
162 "nginx.ingress.kubernetes.io/whitelist-source-range"
163 ] = config.ingress_whitelist_source_range
164
165 if config.cluster_issuer:
166 annotations["cert-manager.io/cluster-issuer"] = config.cluster_issuer
167
168 if parsed.scheme == "https":
169 ingress_resource_builder.add_tls(
170 [parsed.hostname], config.tls_secret_name
171 )
172 else:
173 annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
174
175 ingress_resource_builder.add_rule(
176 parsed.hostname, self.app.name, config.port
177 )
178 ingress_resource = ingress_resource_builder.build()
179 pod_spec_builder.add_ingress_resource(ingress_resource)
180 return pod_spec_builder.build()
181
182
183 if __name__ == "__main__":
184 main(NgUiCharm)