blob: 5efaaaefb85c58d7cf030f9d7312a50cc64cd3b0 [file] [log] [blame]
beierlma4a37f72020-06-26 12:55:01 -04001#!/usr/bin/env python3
David Garcia49379ce2021-02-24 13:48:22 +01002# Copyright 2021 Canonical Ltd.
beierlma4a37f72020-06-26 12:55:01 -04003#
David Garciaef349d92020-12-10 21:16:12 +01004# 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
beierlma4a37f72020-06-26 12:55:01 -04007#
David Garciaef349d92020-12-10 21:16:12 +01008# http://www.apache.org/licenses/LICENSE-2.0
beierlma4a37f72020-06-26 12:55:01 -04009#
David Garciaef349d92020-12-10 21:16:12 +010010# 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##
beierlma4a37f72020-06-26 12:55:01 -040022
David Garcia49379ce2021-02-24 13:48:22 +010023# pylint: disable=E0213
24
25
David Garcia49379ce2021-02-24 13:48:22 +010026from ipaddress import ip_network
David Garciac753dc52021-03-17 15:28:47 +010027import logging
28from pathlib import Path
29from string import Template
30from typing import NoReturn, Optional
David Garcia49379ce2021-02-24 13:48:22 +010031from urllib.parse import urlparse
beierlma4a37f72020-06-26 12:55:01 -040032
beierlma4a37f72020-06-26 12:55:01 -040033from ops.main import main
David Garcia49379ce2021-02-24 13:48:22 +010034from opslib.osm.charm import CharmedOsmBase, RelationsMissing
David Garciac753dc52021-03-17 15:28:47 +010035from opslib.osm.interfaces.http import HttpClient
David Garcia49379ce2021-02-24 13:48:22 +010036from opslib.osm.pod import (
37 ContainerV3Builder,
David Garcia49379ce2021-02-24 13:48:22 +010038 FilesV3Builder,
39 IngressResourceV3Builder,
David Garciac753dc52021-03-17 15:28:47 +010040 PodSpecV3Builder,
David Garcia49379ce2021-02-24 13:48:22 +010041)
David Garciac753dc52021-03-17 15:28:47 +010042from opslib.osm.validator import ModelValidator, validator
David Garcia49379ce2021-02-24 13:48:22 +010043
44
beierlma4a37f72020-06-26 12:55:01 -040045logger = logging.getLogger(__name__)
46
David Garcia49379ce2021-02-24 13:48:22 +010047
48class ConfigModel(ModelValidator):
49 port: int
50 server_name: str
51 max_file_size: int
52 site_url: Optional[str]
sousaedu3cc03162021-04-29 16:53:12 +020053 cluster_issuer: Optional[str]
David Garcia49379ce2021-02-24 13:48:22 +010054 ingress_whitelist_source_range: Optional[str]
55 tls_secret_name: Optional[str]
56
57 @validator("port")
58 def validate_port(cls, v):
59 if v <= 0:
60 raise ValueError("value must be greater than 0")
61 return v
62
63 @validator("max_file_size")
64 def validate_max_file_size(cls, v):
65 if v < 0:
66 raise ValueError("value must be equal or greater than 0")
67 return v
68
69 @validator("site_url")
70 def validate_site_url(cls, v):
71 if v:
72 parsed = urlparse(v)
73 if not parsed.scheme.startswith("http"):
74 raise ValueError("value must start with http")
75 return v
76
77 @validator("ingress_whitelist_source_range")
78 def validate_ingress_whitelist_source_range(cls, v):
79 if v:
80 ip_network(v)
81 return v
beierlma4a37f72020-06-26 12:55:01 -040082
David Garciaef349d92020-12-10 21:16:12 +010083
David Garcia49379ce2021-02-24 13:48:22 +010084class NgUiCharm(CharmedOsmBase):
David Garciaef349d92020-12-10 21:16:12 +010085 def __init__(self, *args) -> NoReturn:
David Garcia49379ce2021-02-24 13:48:22 +010086 super().__init__(*args, oci_image="image")
David Garciaef349d92020-12-10 21:16:12 +010087
David Garcia49379ce2021-02-24 13:48:22 +010088 self.nbi_client = HttpClient(self, "nbi")
89 self.framework.observe(self.on["nbi"].relation_changed, self.configure_pod)
90 self.framework.observe(self.on["nbi"].relation_broken, self.configure_pod)
David Garciaef349d92020-12-10 21:16:12 +010091
David Garcia49379ce2021-02-24 13:48:22 +010092 def _check_missing_dependencies(self, config: ConfigModel):
93 missing_relations = []
beierlma4a37f72020-06-26 12:55:01 -040094
David Garcia49379ce2021-02-24 13:48:22 +010095 if self.nbi_client.is_missing_data_in_app():
96 missing_relations.append("nbi")
David Garciaef349d92020-12-10 21:16:12 +010097
David Garcia49379ce2021-02-24 13:48:22 +010098 if missing_relations:
99 raise RelationsMissing(missing_relations)
David Garciaef349d92020-12-10 21:16:12 +0100100
David Garcia49379ce2021-02-24 13:48:22 +0100101 def _build_files(self, config: ConfigModel):
102 files_builder = FilesV3Builder()
103 files_builder.add_file(
104 "default",
105 Template(Path("files/default").read_text()).substitute(
106 port=config.port,
107 server_name=config.server_name,
108 max_file_size=config.max_file_size,
109 nbi_host=self.nbi_client.host,
110 nbi_port=self.nbi_client.port,
111 ),
beierlma4a37f72020-06-26 12:55:01 -0400112 )
David Garcia49379ce2021-02-24 13:48:22 +0100113 return files_builder.build()
beierlma4a37f72020-06-26 12:55:01 -0400114
David Garcia49379ce2021-02-24 13:48:22 +0100115 def build_pod_spec(self, image_info):
116 # Validate config
117 config = ConfigModel(**dict(self.config))
118 # Check relations
119 self._check_missing_dependencies(config)
120 # Create Builder for the PodSpec
121 pod_spec_builder = PodSpecV3Builder()
122 # Build Container
123 container_builder = ContainerV3Builder(self.app.name, image_info)
124 container_builder.add_port(name=self.app.name, port=config.port)
125 container = container_builder.build()
126 container_builder.add_tcpsocket_readiness_probe(
127 config.port,
128 initial_delay_seconds=45,
129 timeout_seconds=5,
David Garciaef349d92020-12-10 21:16:12 +0100130 )
David Garcia49379ce2021-02-24 13:48:22 +0100131 container_builder.add_tcpsocket_liveness_probe(
132 config.port,
133 initial_delay_seconds=45,
134 timeout_seconds=15,
135 )
136 container_builder.add_volume_config(
137 "configuration",
138 "/etc/nginx/sites-available/",
139 self._build_files(config),
140 )
141 # Add container to pod spec
142 pod_spec_builder.add_container(container)
143 # Add ingress resources to pod spec if site url exists
144 if config.site_url:
145 parsed = urlparse(config.site_url)
146 annotations = {
147 "nginx.ingress.kubernetes.io/proxy-body-size": "{}".format(
148 str(config.max_file_size) + "m"
149 if config.max_file_size > 0
150 else config.max_file_size
151 ),
152 }
153 ingress_resource_builder = IngressResourceV3Builder(
154 f"{self.app.name}-ingress", annotations
David Garciaef349d92020-12-10 21:16:12 +0100155 )
David Garciaef349d92020-12-10 21:16:12 +0100156
David Garcia49379ce2021-02-24 13:48:22 +0100157 if config.ingress_whitelist_source_range:
158 annotations[
159 "nginx.ingress.kubernetes.io/whitelist-source-range"
160 ] = config.ingress_whitelist_source_range
David Garciaef349d92020-12-10 21:16:12 +0100161
sousaedu3cc03162021-04-29 16:53:12 +0200162 if config.cluster_issuer:
163 annotations["cert-manager.io/cluster-issuer"] = config.cluster_issuer
164
David Garcia49379ce2021-02-24 13:48:22 +0100165 if parsed.scheme == "https":
166 ingress_resource_builder.add_tls(
167 [parsed.hostname], config.tls_secret_name
168 )
169 else:
170 annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
David Garciaef349d92020-12-10 21:16:12 +0100171
David Garcia49379ce2021-02-24 13:48:22 +0100172 ingress_resource_builder.add_rule(
173 parsed.hostname, self.app.name, config.port
David Garciaef349d92020-12-10 21:16:12 +0100174 )
David Garcia49379ce2021-02-24 13:48:22 +0100175 ingress_resource = ingress_resource_builder.build()
176 pod_spec_builder.add_ingress_resource(ingress_resource)
177 return pod_spec_builder.build()
beierlma4a37f72020-06-26 12:55:01 -0400178
179
180if __name__ == "__main__":
David Garciaef349d92020-12-10 21:16:12 +0100181 main(NgUiCharm)