2 # Copyright 2021 Canonical Ltd.
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
8 # http://www.apache.org/licenses/LICENSE-2.0
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
16 # For those usages not covered by the Apache License, Version 2.0 please
17 # contact: legal@canonical.com
19 # To get in touch with the maintainers, please contact:
20 # osm-charmers@lists.launchpad.net
23 # pylint: disable=E0213
26 from ipaddress
import ip_network
28 from pathlib
import Path
29 from string
import Template
30 from typing
import NoReturn
, Optional
31 from urllib
.parse
import urlparse
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 (
39 IngressResourceV3Builder
,
42 from opslib
.osm
.validator
import ModelValidator
, validator
45 logger
= logging
.getLogger(__name__
)
48 class ConfigModel(ModelValidator
):
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 image_pull_policy
: str
58 security_context
: bool
61 def validate_port(cls
, v
):
63 raise ValueError("value must be greater than 0")
66 @validator("max_file_size")
67 def validate_max_file_size(cls
, v
):
69 raise ValueError("value must be equal or greater than 0")
72 @validator("site_url")
73 def validate_site_url(cls
, v
):
76 if not parsed
.scheme
.startswith("http"):
77 raise ValueError("value must start with http")
80 @validator("ingress_whitelist_source_range")
81 def validate_ingress_whitelist_source_range(cls
, v
):
86 @validator("image_pull_policy")
87 def validate_image_pull_policy(cls
, v
):
90 "ifnotpresent": "IfNotPresent",
94 if v
not in values
.keys():
95 raise ValueError("value must be always, ifnotpresent or never")
99 class NgUiCharm(CharmedOsmBase
):
100 def __init__(self
, *args
) -> NoReturn
:
101 super().__init
__(*args
, oci_image
="image")
103 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
)
107 def _check_missing_dependencies(self
, config
: ConfigModel
):
108 missing_relations
= []
110 if self
.nbi_client
.is_missing_data_in_app():
111 missing_relations
.append("nbi")
113 if missing_relations
:
114 raise RelationsMissing(missing_relations
)
116 def _build_files(self
, config
: ConfigModel
):
117 files_builder
= FilesV3Builder()
118 files_builder
.add_file(
120 Template(Path("templates/default.template").read_text()).substitute(
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
,
128 return files_builder
.build()
130 def build_pod_spec(self
, image_info
):
132 config
= ConfigModel(**dict(self
.config
))
134 self
._check
_missing
_dependencies
(config
)
135 # Create Builder for the PodSpec
136 pod_spec_builder
= PodSpecV3Builder(
137 enable_security_context
=config
.security_context
140 container_builder
= ContainerV3Builder(
143 config
.image_pull_policy
,
144 run_as_non_root
=config
.security_context
,
146 container_builder
.add_port(name
=self
.app
.name
, port
=config
.port
)
147 container
= container_builder
.build()
148 container_builder
.add_tcpsocket_readiness_probe(
150 initial_delay_seconds
=45,
153 container_builder
.add_tcpsocket_liveness_probe(
155 initial_delay_seconds
=45,
158 container_builder
.add_volume_config(
160 "/etc/nginx/sites-available/",
161 self
._build
_files
(config
),
163 # Add container to pod spec
164 pod_spec_builder
.add_container(container
)
165 # Add ingress resources to pod spec if site url exists
167 parsed
= urlparse(config
.site_url
)
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
175 if config
.ingress_class
:
176 annotations
["kubernetes.io/ingress.class"] = config
.ingress_class
177 ingress_resource_builder
= IngressResourceV3Builder(
178 f
"{self.app.name}-ingress", annotations
181 if config
.ingress_whitelist_source_range
:
183 "nginx.ingress.kubernetes.io/whitelist-source-range"
184 ] = config
.ingress_whitelist_source_range
186 if config
.cluster_issuer
:
187 annotations
["cert-manager.io/cluster-issuer"] = config
.cluster_issuer
189 if parsed
.scheme
== "https":
190 ingress_resource_builder
.add_tls(
191 [parsed
.hostname
], config
.tls_secret_name
194 annotations
["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
196 ingress_resource_builder
.add_rule(
197 parsed
.hostname
, self
.app
.name
, config
.port
199 ingress_resource
= ingress_resource_builder
.build()
200 pod_spec_builder
.add_ingress_resource(ingress_resource
)
201 return pod_spec_builder
.build()
204 if __name__
== "__main__":