Coverage for src/charm.py : 92%
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##
23# pylint: disable=E0213
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
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
45logger = logging.getLogger(__name__)
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]
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
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
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
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
83class NgUiCharm(CharmedOsmBase):
84 def __init__(self, *args) -> NoReturn:
85 super().__init__(*args, oci_image="image")
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)
91 def _check_missing_dependencies(self, config: ConfigModel):
92 missing_relations = []
94 if self.nbi_client.is_missing_data_in_app():
95 missing_relations.append("nbi")
97 if missing_relations:
98 raise RelationsMissing(missing_relations)
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()
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 )
156 if config.ingress_whitelist_source_range:
157 annotations[
158 "nginx.ingress.kubernetes.io/whitelist-source-range"
159 ] = config.ingress_whitelist_source_range
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"
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()
176if __name__ == "__main__":
177 main(NgUiCharm)