blob: 56644f2ef3107763e6a83dd0314713f3f9233cd1 [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]
sousaedu3ddbbd12021-08-24 19:57:24 +010057 image_pull_policy: Optional[str]
David Garcia49379ce2021-02-24 13:48:22 +010058
59 @validator("port")
60 def validate_port(cls, v):
61 if v <= 0:
62 raise ValueError("value must be greater than 0")
63 return v
64
65 @validator("max_file_size")
66 def validate_max_file_size(cls, v):
67 if v < 0:
68 raise ValueError("value must be equal or greater than 0")
69 return v
70
71 @validator("site_url")
72 def validate_site_url(cls, v):
73 if v:
74 parsed = urlparse(v)
75 if not parsed.scheme.startswith("http"):
76 raise ValueError("value must start with http")
77 return v
78
79 @validator("ingress_whitelist_source_range")
80 def validate_ingress_whitelist_source_range(cls, v):
81 if v:
82 ip_network(v)
83 return v
beierlma4a37f72020-06-26 12:55:01 -040084
sousaedu3ddbbd12021-08-24 19:57:24 +010085 @validator("image_pull_policy")
86 def validate_image_pull_policy(cls, v):
87 values = {
88 "always": "Always",
89 "ifnotpresent": "IfNotPresent",
90 "never": "Never",
91 }
92 v = v.lower()
93 if v not in values.keys():
94 raise ValueError("value must be always, ifnotpresent or never")
95 return values[v]
96
David Garciaef349d92020-12-10 21:16:12 +010097
David Garcia49379ce2021-02-24 13:48:22 +010098class NgUiCharm(CharmedOsmBase):
David Garciaef349d92020-12-10 21:16:12 +010099 def __init__(self, *args) -> NoReturn:
David Garcia49379ce2021-02-24 13:48:22 +0100100 super().__init__(*args, oci_image="image")
David Garciaef349d92020-12-10 21:16:12 +0100101
David Garcia49379ce2021-02-24 13:48:22 +0100102 self.nbi_client = HttpClient(self, "nbi")
103 self.framework.observe(self.on["nbi"].relation_changed, self.configure_pod)
104 self.framework.observe(self.on["nbi"].relation_broken, self.configure_pod)
David Garciaef349d92020-12-10 21:16:12 +0100105
David Garcia49379ce2021-02-24 13:48:22 +0100106 def _check_missing_dependencies(self, config: ConfigModel):
107 missing_relations = []
beierlma4a37f72020-06-26 12:55:01 -0400108
David Garcia49379ce2021-02-24 13:48:22 +0100109 if self.nbi_client.is_missing_data_in_app():
110 missing_relations.append("nbi")
David Garciaef349d92020-12-10 21:16:12 +0100111
David Garcia49379ce2021-02-24 13:48:22 +0100112 if missing_relations:
113 raise RelationsMissing(missing_relations)
David Garciaef349d92020-12-10 21:16:12 +0100114
David Garcia49379ce2021-02-24 13:48:22 +0100115 def _build_files(self, config: ConfigModel):
116 files_builder = FilesV3Builder()
117 files_builder.add_file(
118 "default",
David Garciad680be42021-08-17 11:03:55 +0200119 Template(Path("templates/default.template").read_text()).substitute(
David Garcia49379ce2021-02-24 13:48:22 +0100120 port=config.port,
121 server_name=config.server_name,
122 max_file_size=config.max_file_size,
123 nbi_host=self.nbi_client.host,
124 nbi_port=self.nbi_client.port,
125 ),
beierlma4a37f72020-06-26 12:55:01 -0400126 )
David Garcia49379ce2021-02-24 13:48:22 +0100127 return files_builder.build()
beierlma4a37f72020-06-26 12:55:01 -0400128
David Garcia49379ce2021-02-24 13:48:22 +0100129 def build_pod_spec(self, image_info):
130 # Validate config
131 config = ConfigModel(**dict(self.config))
132 # Check relations
133 self._check_missing_dependencies(config)
134 # Create Builder for the PodSpec
135 pod_spec_builder = PodSpecV3Builder()
136 # Build Container
sousaedu3ddbbd12021-08-24 19:57:24 +0100137 container_builder = ContainerV3Builder(
138 self.app.name, image_info, config.image_pull_policy
139 )
David Garcia49379ce2021-02-24 13:48:22 +0100140 container_builder.add_port(name=self.app.name, port=config.port)
141 container = container_builder.build()
142 container_builder.add_tcpsocket_readiness_probe(
143 config.port,
144 initial_delay_seconds=45,
145 timeout_seconds=5,
David Garciaef349d92020-12-10 21:16:12 +0100146 )
David Garcia49379ce2021-02-24 13:48:22 +0100147 container_builder.add_tcpsocket_liveness_probe(
148 config.port,
149 initial_delay_seconds=45,
150 timeout_seconds=15,
151 )
152 container_builder.add_volume_config(
153 "configuration",
154 "/etc/nginx/sites-available/",
155 self._build_files(config),
156 )
157 # Add container to pod spec
158 pod_spec_builder.add_container(container)
159 # Add ingress resources to pod spec if site url exists
160 if config.site_url:
161 parsed = urlparse(config.site_url)
162 annotations = {
163 "nginx.ingress.kubernetes.io/proxy-body-size": "{}".format(
164 str(config.max_file_size) + "m"
165 if config.max_file_size > 0
166 else config.max_file_size
David Garciad68e0b42021-06-28 16:50:42 +0200167 )
David Garcia49379ce2021-02-24 13:48:22 +0100168 }
David Garciad68e0b42021-06-28 16:50:42 +0200169 if config.ingress_class:
170 annotations["kubernetes.io/ingress.class"] = config.ingress_class
David Garcia49379ce2021-02-24 13:48:22 +0100171 ingress_resource_builder = IngressResourceV3Builder(
172 f"{self.app.name}-ingress", annotations
David Garciaef349d92020-12-10 21:16:12 +0100173 )
David Garciaef349d92020-12-10 21:16:12 +0100174
David Garcia49379ce2021-02-24 13:48:22 +0100175 if config.ingress_whitelist_source_range:
176 annotations[
177 "nginx.ingress.kubernetes.io/whitelist-source-range"
178 ] = config.ingress_whitelist_source_range
David Garciaef349d92020-12-10 21:16:12 +0100179
sousaedu3cc03162021-04-29 16:53:12 +0200180 if config.cluster_issuer:
181 annotations["cert-manager.io/cluster-issuer"] = config.cluster_issuer
182
David Garcia49379ce2021-02-24 13:48:22 +0100183 if parsed.scheme == "https":
184 ingress_resource_builder.add_tls(
185 [parsed.hostname], config.tls_secret_name
186 )
187 else:
188 annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
David Garciaef349d92020-12-10 21:16:12 +0100189
David Garcia49379ce2021-02-24 13:48:22 +0100190 ingress_resource_builder.add_rule(
191 parsed.hostname, self.app.name, config.port
David Garciaef349d92020-12-10 21:16:12 +0100192 )
David Garcia49379ce2021-02-24 13:48:22 +0100193 ingress_resource = ingress_resource_builder.build()
194 pod_spec_builder.add_ingress_resource(ingress_resource)
195 return pod_spec_builder.build()
beierlma4a37f72020-06-26 12:55:01 -0400196
197
198if __name__ == "__main__":
David Garciaef349d92020-12-10 21:16:12 +0100199 main(NgUiCharm)