blob: 39675d05732a0831abb3b2e5f6911d8b36e50748 [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 Garciad68e0b42021-06-28 16:50:42 +020054 ingress_class: Optional[str]
David Garcia49379ce2021-02-24 13:48:22 +010055 ingress_whitelist_source_range: Optional[str]
56 tls_secret_name: Optional[str]
sousaedu0dc25b32021-08-30 16:33:33 +010057 image_pull_policy: str
sousaedu540d9372021-09-29 01:53:30 +010058 security_context: bool
David Garcia49379ce2021-02-24 13:48:22 +010059
60 @validator("port")
61 def validate_port(cls, v):
62 if v <= 0:
63 raise ValueError("value must be greater than 0")
64 return v
65
66 @validator("max_file_size")
67 def validate_max_file_size(cls, v):
68 if v < 0:
69 raise ValueError("value must be equal or greater than 0")
70 return v
71
72 @validator("site_url")
73 def validate_site_url(cls, v):
74 if v:
75 parsed = urlparse(v)
76 if not parsed.scheme.startswith("http"):
77 raise ValueError("value must start with http")
78 return v
79
80 @validator("ingress_whitelist_source_range")
81 def validate_ingress_whitelist_source_range(cls, v):
82 if v:
83 ip_network(v)
84 return v
beierlma4a37f72020-06-26 12:55:01 -040085
sousaedu3ddbbd12021-08-24 19:57:24 +010086 @validator("image_pull_policy")
87 def validate_image_pull_policy(cls, v):
88 values = {
89 "always": "Always",
90 "ifnotpresent": "IfNotPresent",
91 "never": "Never",
92 }
93 v = v.lower()
94 if v not in values.keys():
95 raise ValueError("value must be always, ifnotpresent or never")
96 return values[v]
97
David Garciaef349d92020-12-10 21:16:12 +010098
David Garcia49379ce2021-02-24 13:48:22 +010099class NgUiCharm(CharmedOsmBase):
David Garciaef349d92020-12-10 21:16:12 +0100100 def __init__(self, *args) -> NoReturn:
David Garcia49379ce2021-02-24 13:48:22 +0100101 super().__init__(*args, oci_image="image")
David Garciaef349d92020-12-10 21:16:12 +0100102
David Garcia49379ce2021-02-24 13:48:22 +0100103 self.nbi_client = HttpClient(self, "nbi")
104 self.framework.observe(self.on["nbi"].relation_changed, self.configure_pod)
105 self.framework.observe(self.on["nbi"].relation_broken, self.configure_pod)
David Garciaef349d92020-12-10 21:16:12 +0100106
David Garcia49379ce2021-02-24 13:48:22 +0100107 def _check_missing_dependencies(self, config: ConfigModel):
108 missing_relations = []
beierlma4a37f72020-06-26 12:55:01 -0400109
David Garcia49379ce2021-02-24 13:48:22 +0100110 if self.nbi_client.is_missing_data_in_app():
111 missing_relations.append("nbi")
David Garciaef349d92020-12-10 21:16:12 +0100112
David Garcia49379ce2021-02-24 13:48:22 +0100113 if missing_relations:
114 raise RelationsMissing(missing_relations)
David Garciaef349d92020-12-10 21:16:12 +0100115
David Garcia49379ce2021-02-24 13:48:22 +0100116 def _build_files(self, config: ConfigModel):
117 files_builder = FilesV3Builder()
118 files_builder.add_file(
119 "default",
David Garciad680be42021-08-17 11:03:55 +0200120 Template(Path("templates/default.template").read_text()).substitute(
David Garcia49379ce2021-02-24 13:48:22 +0100121 port=config.port,
122 server_name=config.server_name,
123 max_file_size=config.max_file_size,
124 nbi_host=self.nbi_client.host,
125 nbi_port=self.nbi_client.port,
126 ),
beierlma4a37f72020-06-26 12:55:01 -0400127 )
David Garcia49379ce2021-02-24 13:48:22 +0100128 return files_builder.build()
beierlma4a37f72020-06-26 12:55:01 -0400129
David Garcia49379ce2021-02-24 13:48:22 +0100130 def build_pod_spec(self, image_info):
131 # Validate config
132 config = ConfigModel(**dict(self.config))
133 # Check relations
134 self._check_missing_dependencies(config)
135 # Create Builder for the PodSpec
sousaedu540d9372021-09-29 01:53:30 +0100136 pod_spec_builder = PodSpecV3Builder(
137 enable_security_context=config.security_context
138 )
David Garcia49379ce2021-02-24 13:48:22 +0100139 # Build Container
sousaedu3ddbbd12021-08-24 19:57:24 +0100140 container_builder = ContainerV3Builder(
sousaedu540d9372021-09-29 01:53:30 +0100141 self.app.name,
142 image_info,
143 config.image_pull_policy,
144 run_as_non_root=config.security_context,
sousaedu3ddbbd12021-08-24 19:57:24 +0100145 )
David Garcia49379ce2021-02-24 13:48:22 +0100146 container_builder.add_port(name=self.app.name, port=config.port)
147 container = container_builder.build()
148 container_builder.add_tcpsocket_readiness_probe(
149 config.port,
150 initial_delay_seconds=45,
151 timeout_seconds=5,
David Garciaef349d92020-12-10 21:16:12 +0100152 )
David Garcia49379ce2021-02-24 13:48:22 +0100153 container_builder.add_tcpsocket_liveness_probe(
154 config.port,
155 initial_delay_seconds=45,
156 timeout_seconds=15,
157 )
158 container_builder.add_volume_config(
159 "configuration",
160 "/etc/nginx/sites-available/",
161 self._build_files(config),
162 )
163 # Add container to pod spec
164 pod_spec_builder.add_container(container)
165 # Add ingress resources to pod spec if site url exists
166 if config.site_url:
167 parsed = urlparse(config.site_url)
168 annotations = {
169 "nginx.ingress.kubernetes.io/proxy-body-size": "{}".format(
170 str(config.max_file_size) + "m"
171 if config.max_file_size > 0
172 else config.max_file_size
David Garciad68e0b42021-06-28 16:50:42 +0200173 )
David Garcia49379ce2021-02-24 13:48:22 +0100174 }
David Garciad68e0b42021-06-28 16:50:42 +0200175 if config.ingress_class:
176 annotations["kubernetes.io/ingress.class"] = config.ingress_class
David Garcia49379ce2021-02-24 13:48:22 +0100177 ingress_resource_builder = IngressResourceV3Builder(
178 f"{self.app.name}-ingress", annotations
David Garciaef349d92020-12-10 21:16:12 +0100179 )
David Garciaef349d92020-12-10 21:16:12 +0100180
David Garcia49379ce2021-02-24 13:48:22 +0100181 if config.ingress_whitelist_source_range:
182 annotations[
183 "nginx.ingress.kubernetes.io/whitelist-source-range"
184 ] = config.ingress_whitelist_source_range
David Garciaef349d92020-12-10 21:16:12 +0100185
sousaedu3cc03162021-04-29 16:53:12 +0200186 if config.cluster_issuer:
187 annotations["cert-manager.io/cluster-issuer"] = config.cluster_issuer
188
David Garcia49379ce2021-02-24 13:48:22 +0100189 if parsed.scheme == "https":
190 ingress_resource_builder.add_tls(
191 [parsed.hostname], config.tls_secret_name
192 )
193 else:
194 annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
David Garciaef349d92020-12-10 21:16:12 +0100195
David Garcia49379ce2021-02-24 13:48:22 +0100196 ingress_resource_builder.add_rule(
197 parsed.hostname, self.app.name, config.port
David Garciaef349d92020-12-10 21:16:12 +0100198 )
David Garcia49379ce2021-02-24 13:48:22 +0100199 ingress_resource = ingress_resource_builder.build()
200 pod_spec_builder.add_ingress_resource(ingress_resource)
201 return pod_spec_builder.build()
beierlma4a37f72020-06-26 12:55:01 -0400202
203
204if __name__ == "__main__":
David Garciaef349d92020-12-10 21:16:12 +0100205 main(NgUiCharm)