From ef349d9224f93fcc3eeb7a26f71c6a128ffbf96a Mon Sep 17 00:00:00 2001 From: David Garcia Date: Thu, 10 Dec 2020 21:16:12 +0100 Subject: [PATCH] Migrate to new NBI charm, and change in NGUI charm - Fixes bug 1365 - Changes in the installer: - use site_url config to expose both NBI and NG-UI - use resources for images - Changes in the NBI: some minor fixes - Changes in the NG-UI: follow the same pattern as in the new set of charms Change-Id: I6a11009ddf9cd22689602b2a53ecf67f376830cb Signed-off-by: David Garcia --- installers/charm/build.sh | 20 +- installers/charm/nbi/src/charm.py | 19 +- installers/charm/nbi/src/pod_spec.py | 2 +- installers/charm/ng-ui/README.md | 16 +- installers/charm/ng-ui/config.yaml | 47 ++-- installers/charm/ng-ui/files/default | 10 +- installers/charm/ng-ui/metadata.yaml | 5 + installers/charm/ng-ui/requirements.txt | 2 + installers/charm/ng-ui/src/charm.py | 335 +++++++++++------------- installers/charm/ng-ui/src/pod_spec.py | 296 +++++++++++++++++++++ installers/charm/release_edge.sh | 54 ++++ installers/charmed_install.sh | 62 +++-- 12 files changed, 601 insertions(+), 267 deletions(-) create mode 100644 installers/charm/ng-ui/src/pod_spec.py create mode 100644 installers/charm/release_edge.sh diff --git a/installers/charm/build.sh b/installers/charm/build.sh index 1c017ba1..edcad538 100755 --- a/installers/charm/build.sh +++ b/installers/charm/build.sh @@ -13,8 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -set -eux - function build() { cd $1 && tox -e build && cd .. } @@ -24,7 +22,17 @@ build 'mon-k8s' build 'nbi-k8s' build 'pol-k8s' build 'ro-k8s' -build 'ui-k8s' -build 'keystone' -build 'ng-ui' -build 'pla' \ No newline at end of file +# build 'ui-k8s' + +charms="nbi pla pol mon lcm ng-ui keystone" +charms="nbi" +if [ -z `which charmcraft` ]; then + sudo snap install charmcraft --beta +fi + +for charm_directory in $charms; do + echo "Building charm $charm_directory..." + cd $charm_directory + charmcraft build + cd .. +done diff --git a/installers/charm/nbi/src/charm.py b/installers/charm/nbi/src/charm.py index 6db99be6..f2b2ecf7 100755 --- a/installers/charm/nbi/src/charm.py +++ b/installers/charm/nbi/src/charm.py @@ -143,7 +143,7 @@ class NbiCharm(CharmBase): ) ): self.state.message_host = message_host - self.state.message_port = message_port + self.state.message_port = int(message_port) self.on.configure_pod.emit() def _on_kafka_relation_departed(self, event: EventBase) -> NoReturn: @@ -219,7 +219,7 @@ class NbiCharm(CharmBase): ) ): self.state.keystone_host = keystone_host - self.state.keystone_port = keystone_port + self.state.keystone_port = int(keystone_port) self.state.keystone_user_domain_name = keystone_user_domain_name self.state.keystone_project_domain_name = keystone_project_domain_name self.state.keystone_username = keystone_username @@ -262,7 +262,7 @@ class NbiCharm(CharmBase): ) ): self.state.prometheus_host = prometheus_host - self.state.prometheus_port = prometheus_port + self.state.prometheus_port = int(prometheus_port) self.on.configure_pod.emit() def _on_prometheus_relation_departed(self, event: EventBase) -> NoReturn: @@ -281,13 +281,12 @@ class NbiCharm(CharmBase): Args: event (EventBase): NBI relation event. """ - if self.unit.is_leader(): - rel_data = { - "host": self.model.app.name, - "port": str(NBI_PORT), - } - for k, v in rel_data.items(): - event.relation.data[self.model.app][k] = v + rel_data = { + "host": self.model.app.name, + "port": str(NBI_PORT), + } + for k, v in rel_data.items(): + event.relation.data[self.unit][k] = v def _missing_relations(self) -> str: """Checks if there missing relations. diff --git a/installers/charm/nbi/src/pod_spec.py b/installers/charm/nbi/src/pod_spec.py index 77fff3e3..96518b9a 100644 --- a/installers/charm/nbi/src/pod_spec.py +++ b/installers/charm/nbi/src/pod_spec.py @@ -270,7 +270,7 @@ def _make_pod_ingress_resources( annotations = { "nginx.ingress.kubernetes.io/proxy-body-size": "{}".format( - max_file_size + "m" if max_file_size > 0 else max_file_size + str(max_file_size) + "m" if max_file_size > 0 else max_file_size ), "nginx.ingress.kubernetes.io/backend-protocol": "HTTPS", } diff --git a/installers/charm/ng-ui/README.md b/installers/charm/ng-ui/README.md index e6a3d583..9b77b5d5 100644 --- a/installers/charm/ng-ui/README.md +++ b/installers/charm/ng-ui/README.md @@ -18,13 +18,13 @@ ```bash juju deploy . # cs:~charmed-osm/ng-ui --channel edge -juju relate ng-ui nbi-k8s +juju relate ng-ui nbi ``` ## How to expose the NG-UI through ingress ```bash -juju config ng-ui juju-external-hostname=ng..xip.io +juju config ng-ui site_url=ng..xip.io juju expose ng-ui ``` @@ -36,16 +36,6 @@ juju expose ng-ui juju scale-application ng-ui 3 ``` -## How to use certificates - -Generate your own certificate if you don't have one already: - -```bash -sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout ssl_certificate.key -out ssl_certificate.crt -sudo chown $USER:$USER ssl_certificate.key -juju config ng-ui ssl_certificate=`cat ssl_certificate.crt | base64 -w 0` -juju config ng-ui ssl_certificate_key=`cat ssl_certificate.key | base64 -w 0` -``` ## Config Examples @@ -53,5 +43,5 @@ juju config ng-ui ssl_certificate_key=`cat ssl_certificate.key | base64 -w 0` juju config ng-ui image=opensourcemano/ng-ui: juju config ng-ui port=80 juju config server_name= -juju config client_max_body_size=25M +juju config max_file_size=25 ``` diff --git a/installers/charm/ng-ui/config.yaml b/installers/charm/ng-ui/config.yaml index 022d150b..279b7591 100644 --- a/installers/charm/ng-ui/config.yaml +++ b/installers/charm/ng-ui/config.yaml @@ -16,37 +16,32 @@ # limitations under the License. options: - image: - description: Docker image name - type: string - default: opensourcemano/ng-ui:8 - image_username: - description: Docker repository username - type: string - default: "" - image_password: - description: Docker repository password + server_name: + description: Server name type: string - default: "" + default: localhost port: - description: Port number + description: Port to expose type: int default: 80 - https_port: - description: Port number + max_file_size: type: int - default: 443 - server_name: - description: Server name - type: string - default: localhost - client_max_body_size: - description: Client maximum body size + description: | + The maximum file size, in megabytes. If there is a reverse proxy in front + of Keystone, it may need to be configured to handle the requested size. + Note: if set to 0, there is no limit. + default: 0 + ingress_whitelist_source_range: type: string - default: 15M - ssl_certificate: - description: Base64 encoded ssl certificate + description: | + A comma-separated list of CIDRs to store in the + ingress.kubernetes.io/whitelist-source-range annotation. + default: "" + tls_secret_name: type: string - ssl_certificate_key: - description: Base64 encoded ssl certificate key + description: TLS Secret name + default: "" + site_url: type: string + description: Ingress URL + default: "" diff --git a/installers/charm/ng-ui/files/default b/installers/charm/ng-ui/files/default index e0147154..f946263f 100644 --- a/installers/charm/ng-ui/files/default +++ b/installers/charm/ng-ui/files/default @@ -15,16 +15,12 @@ server { - listen $http_port; - listen $https_port default ssl; + listen $port; server_name $server_name; root /usr/share/nginx/html; index index.html index.htm; - client_max_body_size $client_max_body_size; - $ssl_crt - $ssl_crt_key - ssl_protocols TLSv1 TLSv1.1 TLSv1.2; - ssl_ciphers EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH; + client_max_body_size $max_file_size; + location /osm { proxy_pass https://$nbi_host:$nbi_port; proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504; diff --git a/installers/charm/ng-ui/metadata.yaml b/installers/charm/ng-ui/metadata.yaml index a0a59210..316047ff 100644 --- a/installers/charm/ng-ui/metadata.yaml +++ b/installers/charm/ng-ui/metadata.yaml @@ -25,3 +25,8 @@ deployment: requires: nbi: interface: osm-nbi +resources: + image: + type: oci-image + description: OSM docker image for NBI + upstream-source: "opensourcemano/ng-ui:8" diff --git a/installers/charm/ng-ui/requirements.txt b/installers/charm/ng-ui/requirements.txt index 10ecdcd5..a178e334 100644 --- a/installers/charm/ng-ui/requirements.txt +++ b/installers/charm/ng-ui/requirements.txt @@ -12,3 +12,5 @@ # See the License for the specific language governing permissions and # limitations under the License. ops +pydantic +git+https://github.com/juju-solutions/resource-oci-image/@c5778285d332edf3d9a538f9d0c06154b7ec1b0b#egg=oci-image diff --git a/installers/charm/ng-ui/src/charm.py b/installers/charm/ng-ui/src/charm.py index 6f5ca5b5..7510a6cb 100755 --- a/installers/charm/ng-ui/src/charm.py +++ b/installers/charm/ng-ui/src/charm.py @@ -1,209 +1,194 @@ #!/usr/bin/env python3 -# Copyright 2020 Canonical Ltd. +# Copyright 2020 Canonical Ltd. # -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import base64 -from glob import glob +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact: legal@canonical.com +# +# To get in touch with the maintainers, please contact: +# osm-charmers@lists.launchpad.net +## + import logging -from pathlib import Path -from string import Template -import sys +from typing import Any, Dict, NoReturn +from pydantic import ValidationError -from ops.charm import CharmBase -from ops.framework import StoredState, Object +from ops.charm import CharmBase, CharmEvents +from ops.framework import EventBase, EventSource, StoredState from ops.main import main -from ops.model import ( - ActiveStatus, - MaintenanceStatus, - BlockedStatus, - ModelError, - WaitingStatus, -) +from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus +from oci_image import OCIImageResource, OCIImageResourceError +from pod_spec import make_pod_spec logger = logging.getLogger(__name__) +NGUI_PORT = 80 + + +class ConfigurePodEvent(EventBase): + """Configure Pod event""" + + pass + + +class NgUiEvents(CharmEvents): + """NGUI Events""" + + configure_pod = EventSource(ConfigurePodEvent) + + +class NgUiCharm(CharmBase): + """NGUI Charm.""" -class NGUICharm(CharmBase): state = StoredState() + on = NgUiEvents() + + def __init__(self, *args) -> NoReturn: + """NGUI Charm constructor.""" + super().__init__(*args) - def __init__(self, framework, key): - super().__init__(framework, key) - self.state.set_default(spec=None) + # Internal state initialization + self.state.set_default(pod_spec=None) + + # North bound interface initialization self.state.set_default(nbi_host=None) self.state.set_default(nbi_port=None) - # Observe Charm related events - self.framework.observe(self.on.config_changed, self.on_config_changed) - self.framework.observe(self.on.start, self.on_start) - self.framework.observe(self.on.upgrade_charm, self.on_upgrade_charm) + self.http_port = NGUI_PORT + self.image = OCIImageResource(self, "image") + + # Registering regular events + self.framework.observe(self.on.start, self.configure_pod) + self.framework.observe(self.on.config_changed, self.configure_pod) + # self.framework.observe(self.on.upgrade_charm, self.configure_pod) + + # Registering custom internal events + self.framework.observe(self.on.configure_pod, self.configure_pod) + + # Registering required relation changed events self.framework.observe( - self.on.nbi_relation_changed, self.on_nbi_relation_changed + self.on.nbi_relation_changed, self._on_nbi_relation_changed ) - # SSL Certificate path - self.ssl_folder = "/certs" - self.ssl_crt_name = "ssl_certificate.crt" - self.ssl_key_name = "ssl_certificate.key" - - def _apply_spec(self): - # Only apply the spec if this unit is a leader. - unit = self.model.unit - if not unit.is_leader(): - unit.status = ActiveStatus("ready") - return - if not self.state.nbi_host or not self.state.nbi_port: - unit.status = WaitingStatus("Waiting for NBI") - return - unit.status = MaintenanceStatus("Applying new pod spec") + # Registering required relation departed events + self.framework.observe( + self.on.nbi_relation_departed, self._on_nbi_relation_departed + ) - new_spec = self.make_pod_spec() - if new_spec == self.state.spec: - unit.status = ActiveStatus("ready") - return - self.framework.model.pod.set_spec(new_spec) - self.state.spec = new_spec - unit.status = ActiveStatus("ready") - - def make_pod_spec(self): - config = self.framework.model.config - - config_spec = { - "http_port": config["port"], - "https_port": config["https_port"], - "server_name": config["server_name"], - "client_max_body_size": config["client_max_body_size"], - "nbi_host": self.state.nbi_host or config["nbi_host"], - "nbi_port": self.state.nbi_port or config["nbi_port"], - "ssl_crt": "", - "ssl_crt_key": "", + def _on_nbi_relation_changed(self, event: EventBase) -> NoReturn: + """Reads information about the nbi relation. + + Args: + event (EventBase): NBI relation event. + """ + data_loc = event.unit if event.unit else event.app + logger.error(dict(event.relation.data)) + nbi_host = event.relation.data[data_loc].get("host") + nbi_port = event.relation.data[data_loc].get("port") + + if ( + nbi_host + and nbi_port + and (self.state.nbi_host != nbi_host or self.state.nbi_port != nbi_port) + ): + self.state.nbi_host = nbi_host + self.state.nbi_port = nbi_port + self.on.configure_pod.emit() + + def _on_nbi_relation_departed(self, event: EventBase) -> NoReturn: + """Clears data from nbi relation. + + Args: + event (EventBase): NBI relation event. + """ + self.state.nbi_host = None + self.state.nbi_port = None + self.on.configure_pod.emit() + + def _missing_relations(self) -> str: + """Checks if there missing relations. + + Returns: + str: string with missing relations + """ + data_status = { + "nbi": self.state.nbi_host, } - ssl_certificate = None - ssl_certificate_key = None - ssl_enabled = False - - if "ssl_certificate" in config and "ssl_certificate_key" in config: - # Get bytes of cert and key - cert_b = base64.b64decode(config["ssl_certificate"]) - key_b = base64.b64decode(config["ssl_certificate_key"]) - # Decode key and cert - ssl_certificate = cert_b.decode("utf-8") - ssl_certificate_key = key_b.decode("utf-8") - # Get paths - cert_path = "{}/{}".format(self.ssl_folder, self.ssl_crt_name) - key_path = "{}/{}".format(self.ssl_folder, self.ssl_key_name) - - config_spec["port"] = "{} ssl".format(config["https_port"]) - config_spec["ssl_crt"] = "ssl_certificate {};".format(cert_path) - config_spec["ssl_crt_key"] = "ssl_certificate_key {};".format(key_path) - ssl_enabled = True - else: - config_spec["ssl_crt"] = "" - config_spec["ssl_crt_key"] = "" - - files = [ - { - "name": "configuration", - "mountPath": "/etc/nginx/sites-available/", - "files": { - Path(filename) - .name: Template(Path(filename).read_text()) - .substitute(config_spec) - for filename in glob("files/*") - }, - } - ] - port = config["https_port"] if ssl_enabled else config["port"] - ports = [ - { - "name": "port", - "containerPort": port, - "protocol": "TCP", - }, - ] - - kubernetes = { - "readinessProbe": { - "tcpSocket": {"port": port}, - "timeoutSeconds": 5, - "periodSeconds": 5, - "initialDelaySeconds": 10, - }, - "livenessProbe": { - "tcpSocket": {"port": port}, - "timeoutSeconds": 5, - "initialDelaySeconds": 45, - }, - } + missing_relations = [k for k, v in data_status.items() if not v] - if ssl_certificate and ssl_certificate_key: - files.append( - { - "name": "ssl", - "mountPath": self.ssl_folder, - "files": { - self.ssl_crt_name: ssl_certificate, - self.ssl_key_name: ssl_certificate_key, - }, - } - ) + return ", ".join(missing_relations) + + @property + def relation_state(self) -> Dict[str, Any]: + """Collects relation state configuration for pod spec assembly. - logger.debug(files) - - spec = { - "version": 2, - "containers": [ - { - "name": self.framework.model.app.name, - "imageDetails": { - "imagePath": config["image"], - "username": config["image_username"], - "password": config["image_password"], - }, - "ports": ports, - "kubernetes": kubernetes, - "files": files, - } - ], + Returns: + Dict[str, Any]: relation state information. + """ + relation_state = { + "nbi_host": self.state.nbi_host, + "nbi_port": self.state.nbi_port, } + return relation_state + + def configure_pod(self, event: EventBase) -> NoReturn: + """Assemble the pod spec and apply it, if possible. + + Args: + event (EventBase): Hook or Relation event that started the + function. + """ + if missing := self._missing_relations(): + self.unit.status = BlockedStatus( + f"Waiting for {missing} relation{'s' if ',' in missing else ''}" + ) + return - return spec + if not self.unit.is_leader(): + self.unit.status = ActiveStatus("ready") + return - def on_config_changed(self, event): - """Handle changes in configuration""" - self._apply_spec() + self.unit.status = MaintenanceStatus("Assembling pod spec") - def on_start(self, event): - """Called when the charm is being installed""" - self._apply_spec() + # Fetch image information + try: + self.unit.status = MaintenanceStatus("Fetching image information") + image_info = self.image.fetch() + except OCIImageResourceError: + self.unit.status = BlockedStatus("Error fetching image information") + return - def on_upgrade_charm(self, event): - """Upgrade the charm.""" - unit = self.model.unit - unit.status = MaintenanceStatus("Upgrading charm") - self.on_start(event) + try: + pod_spec = make_pod_spec( + image_info, + self.config, + self.relation_state, + self.model.app.name, + ) + except ValidationError as exc: + logger.exception("Config/Relation data validation error") + self.unit.status = BlockedStatus(str(exc)) + return - def on_nbi_relation_changed(self, event): - nbi_host = event.relation.data[event.unit].get("host") - nbi_port = event.relation.data[event.unit].get("port") - if nbi_host and self.state.nbi_host != nbi_host: - self.state.nbi_host = nbi_host - if nbi_port and self.state.nbi_port != nbi_port: - self.state.nbi_port = nbi_port - self._apply_spec() + if self.state.pod_spec != pod_spec: + self.model.pod.set_spec(pod_spec) + self.state.pod_spec = pod_spec + + self.unit.status = ActiveStatus("ready") if __name__ == "__main__": - main(NGUICharm) + main(NgUiCharm) diff --git a/installers/charm/ng-ui/src/pod_spec.py b/installers/charm/ng-ui/src/pod_spec.py new file mode 100644 index 00000000..1687756a --- /dev/null +++ b/installers/charm/ng-ui/src/pod_spec.py @@ -0,0 +1,296 @@ +#!/usr/bin/env python3 +# Copyright 2020 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact: legal@canonical.com +# +# To get in touch with the maintainers, please contact: +# osm-charmers@lists.launchpad.net +## + +import logging +from pydantic import ( + BaseModel, + conint, + IPvAnyNetwork, + PositiveInt, + validator, +) +from typing import Any, Dict, List, Optional +from urllib.parse import urlparse +from pathlib import Path +from string import Template + +logger = logging.getLogger(__name__) + + +class ConfigData(BaseModel): + """Configuration data model.""" + + port: PositiveInt + site_url: Optional[str] + max_file_size: Optional[conint(ge=0)] + ingress_whitelist_source_range: Optional[IPvAnyNetwork] + tls_secret_name: Optional[str] + + @validator("max_file_size", pre=True, always=True) + def validate_max_file_size(cls, value, values, **kwargs): + site_url = values.get("site_url") + + if not site_url: + return value + + parsed = urlparse(site_url) + + if not parsed.scheme.startswith("http"): + return value + + if value is None: + raise ValueError("max_file_size needs to be defined if site_url is defined") + + return value + + @validator("ingress_whitelist_source_range", pre=True, always=True) + def validate_ingress_whitelist_source_range(cls, value, values, **kwargs): + if not value: + return None + + return value + + +class RelationData(BaseModel): + """Relation data model.""" + + nbi_host: str + nbi_port: PositiveInt + + +def _make_pod_ports(port: int) -> List[Dict[str, Any]]: + """Generate pod ports details. + + Args: + port (int): Port to expose. + + Returns: + List[Dict[str, Any]]: pod port details. + """ + return [ + {"name": "http", "containerPort": port, "protocol": "TCP"}, + ] + + +def _make_pod_ingress_resources( + config: Dict[str, Any], app_name: str, port: int +) -> List[Dict[str, Any]]: + """Generate pod ingress resources. + + Args: + config (Dict[str, Any]): configuration information. + app_name (str): application name. + port (int): port to expose. + + Returns: + List[Dict[str, Any]]: pod ingress resources. + """ + site_url = config.get("site_url") + + if not site_url: + return + + parsed = urlparse(site_url) + + if not parsed.scheme.startswith("http"): + return + + max_file_size = config["max_file_size"] + ingress_whitelist_source_range = config["ingress_whitelist_source_range"] + + annotations = { + "nginx.ingress.kubernetes.io/proxy-body-size": "{}".format( + str(max_file_size) + "m" if max_file_size > 0 else max_file_size + ), + } + + if ingress_whitelist_source_range: + annotations[ + "nginx.ingress.kubernetes.io/whitelist-source-range" + ] = ingress_whitelist_source_range + + ingress_spec_tls = None + + if parsed.scheme == "https": + ingress_spec_tls = [{"hosts": [parsed.hostname]}] + tls_secret_name = config["tls_secret_name"] + if tls_secret_name: + ingress_spec_tls[0]["secretName"] = tls_secret_name + else: + annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false" + + ingress = { + "name": "{}-ingress".format(app_name), + "annotations": annotations, + "spec": { + "rules": [ + { + "host": parsed.hostname, + "http": { + "paths": [ + { + "path": "/", + "backend": { + "serviceName": app_name, + "servicePort": port, + }, + } + ] + }, + } + ] + }, + } + if ingress_spec_tls: + ingress["spec"]["tls"] = ingress_spec_tls + + return [ingress] + + +def _make_startup_probe() -> Dict[str, Any]: + """Generate startup probe. + + Returns: + Dict[str, Any]: startup probe. + """ + return { + "exec": {"command": ["/usr/bin/pgrep python3"]}, + "initialDelaySeconds": 60, + "timeoutSeconds": 5, + } + + +def _make_readiness_probe(port: int) -> Dict[str, Any]: + """Generate readiness probe. + + Args: + port (int): [description] + + Returns: + Dict[str, Any]: readiness probe. + """ + return { + "tcpSocket": { + "port": port, + }, + "initialDelaySeconds": 45, + "timeoutSeconds": 5, + } + + +def _make_liveness_probe(port: int) -> Dict[str, Any]: + """Generate liveness probe. + + Args: + port (int): [description] + + Returns: + Dict[str, Any]: liveness probe. + """ + return { + "tcpSocket": { + "port": port, + }, + "initialDelaySeconds": 45, + "timeoutSeconds": 5, + } + + +def _make_pod_volume_config( + config: Dict[str, Any], + relation_state: Dict[str, Any], +) -> List[Dict[str, Any]]: + """Generate volume config with files. + + Args: + config (Dict[str, Any]): configuration information. + + Returns: + Dict[str, Any]: volume config. + """ + template_data = {**config, **relation_state} + template_data["max_file_size"] = f'{template_data["max_file_size"]}M' + return [ + { + "name": "configuration", + "mountPath": "/etc/nginx/sites-available/", + "files": [ + { + "path": "default", + "content": Template(Path("files/default").read_text()).substitute( + template_data + ), + } + ], + } + ] + + +def make_pod_spec( + image_info: Dict[str, str], + config: Dict[str, Any], + relation_state: Dict[str, Any], + app_name: str = "ng-ui", +) -> Dict[str, Any]: + """Generate the pod spec information. + + Args: + image_info (Dict[str, str]): Object provided by + OCIImageResource("image").fetch(). + config (Dict[str, Any]): Configuration information. + relation_state (Dict[str, Any]): Relation state information. + app_name (str, optional): Application name. Defaults to "ng-ui". + port (int, optional): Port for the container. Defaults to 80. + + Returns: + Dict[str, Any]: Pod spec dictionary for the charm. + """ + if not image_info: + return None + + ConfigData(**(config)) + RelationData(**(relation_state)) + + ports = _make_pod_ports(config["port"]) + ingress_resources = _make_pod_ingress_resources(config, app_name, config["port"]) + kubernetes = { + # "startupProbe": _make_startup_probe(), + "readinessProbe": _make_readiness_probe(config["port"]), + "livenessProbe": _make_liveness_probe(config["port"]), + } + volume_config = _make_pod_volume_config(config, relation_state) + return { + "version": 3, + "containers": [ + { + "name": app_name, + "imageDetails": image_info, + "imagePullPolicy": "Always", + "ports": ports, + "kubernetes": kubernetes, + "volumeConfig": volume_config, + } + ], + "kubernetesResources": { + "ingressResources": ingress_resources or [], + }, + } diff --git a/installers/charm/release_edge.sh b/installers/charm/release_edge.sh new file mode 100644 index 00000000..a3d698e8 --- /dev/null +++ b/installers/charm/release_edge.sh @@ -0,0 +1,54 @@ +#!/bin/bash +# Copyright 2020 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +set -eux + +channel=edge +tag=testing-daily + +# 1. Build charms +./build.sh + +# 2. Release charms +# Reactive charms +charms="lcm-k8s mon-k8s pol-k8s ro-k8s" +charms="" +for charm in $charms; do + cs_revision=`charm push $charm/release cs:~charmed-osm/$charm | tail -n +1 | head -1 | awk '{print $2}'` + charm release --channel $channel $cs_revision + echo "$charm charm released!" +done + +# New charms (with no resources) +charms="pla keystone" +charms="" +for charm in $charms; do + echo "Releasing $charm charm" + cs_revision=`charm push $charm/$charm.charm cs:~charmed-osm/$charm | tail -n +1 | head -1 | awk '{print $2}'` + charm release --channel $channel $cs_revision + echo "$charm charm released!" +done + +# New charms (with resources) +charms="ng-ui nbi" +charms="nbi" +for charm in $charms; do + echo "Releasing $charm charm" + cs_revision=$(charm push $charm/$charm.charm cs:~charmed-osm/$charm | tail -n +1 | head -1 | awk '{print $2}') + resource_revision=$(charm attach $cs_revision image=external::opensourcemano/$charm:$tag | tail -n +1 | sed 's/[^0-9]*//g') + image_revision_num=$(echo $resource_revision | awk '{print $NF}') + resources_string="--resource image-$image_revision_num" + charm release --channel $channel $cs_revision $resources_string + echo "$charm charm released!" +done diff --git a/installers/charmed_install.sh b/installers/charmed_install.sh index 095c04f3..16d6d731 100755 --- a/installers/charmed_install.sh +++ b/installers/charmed_install.sh @@ -22,6 +22,8 @@ MICROK8S_VERSION=1.19 IMAGES_OVERLAY_FILE=~/.osm/images-overlay.yaml PATH=/snap/bin:${PATH} +MODEL_NAME=osm + function check_arguments(){ while [ $# -gt 0 ] ; do case $1 in @@ -224,9 +226,9 @@ function deploy_charmed_osm(){ create_overlay echo "Creating OSM model" if [ -v KUBECFG ]; then - juju add-model osm $K8S_CLOUD_NAME + juju add-model $MODEL_NAME $K8S_CLOUD_NAME else - sg ${KUBEGRP} -c "juju add-model osm $K8S_CLOUD_NAME" + sg ${KUBEGRP} -c "juju add-model $MODEL_NAME $K8S_CLOUD_NAME" fi echo "Deploying OSM with charms" images_overlay="" @@ -236,9 +238,9 @@ function deploy_charmed_osm(){ [ -v TAG ] && generate_images_overlay && images_overlay="--overlay $IMAGES_OVERLAY_FILE" if [ -v BUNDLE ]; then - juju deploy $BUNDLE --overlay ~/.osm/vca-overlay.yaml $images_overlay + juju deploy -m $MODEL_NAME $BUNDLE --overlay ~/.osm/vca-overlay.yaml $images_overlay else - juju deploy cs:osm-52 --overlay ~/.osm/vca-overlay.yaml $images_overlay + juju deploy -m $MODEL_NAME cs:osm-53 --overlay ~/.osm/vca-overlay.yaml $images_overlay fi echo "Waiting for deployment to finish..." @@ -256,29 +258,21 @@ function deploy_charmed_osm(){ fi # Expose OSM services - # Expose Grafana - juju config grafana-k8s juju-external-hostname=grafana.${API_SERVER}.xip.io - juju expose grafana-k8s - wait_for_port grafana-k8s 0 - # Expose NBI - juju config nbi-k8s juju-external-hostname=nbi.${API_SERVER}.xip.io - juju expose nbi-k8s - wait_for_port nbi-k8s 1 + juju config -m $MODEL_NAME nbi site_url=https://nbi.${API_SERVER}.xip.io + juju config -m $MODEL_NAME ng-ui site_url=https://ui.${API_SERVER}.xip.io - # Expose NG UI - juju config ng-ui juju-external-hostname=ui.${API_SERVER}.xip.io - juju expose ng-ui - wait_for_port ng-ui 2 + # Expose Grafana + juju config -m $MODEL_NAME grafana-k8s juju-external-hostname=grafana.${API_SERVER}.xip.io + juju expose -m $MODEL_NAME grafana-k8s + wait_for_port grafana-k8s 0 # Expose Prometheus - juju config prometheus-k8s juju-external-hostname=prometheus.${API_SERVER}.xip.io - juju expose prometheus-k8s - wait_for_port prometheus-k8s 3 + juju config -m $MODEL_NAME prometheus-k8s juju-external-hostname=prometheus.${API_SERVER}.xip.io + juju expose -m $MODEL_NAME prometheus-k8s + wait_for_port prometheus-k8s 1 # Apply annotations - sg ${KUBEGRP} -c "${KUBECTL} annotate ingresses.networking nginx.ingress.kubernetes.io/backend-protocol=HTTPS -n osm -l juju-app=nbi-k8s" - sg ${KUBEGRP} -c "${KUBECTL} annotate ingresses.networking nginx.ingress.kubernetes.io/proxy-body-size=0 -n osm -l juju-app=nbi-k8s" sg ${KUBEGRP} -c "${KUBECTL} annotate ingresses.networking nginx.ingress.kubernetes.io/proxy-body-size=0 -n osm -l juju-app=ng-ui" } @@ -289,7 +283,7 @@ function check_osm_deployed() { previous_count=0 while true do - service_count=$(juju status | grep kubernetes | grep active | wc -l) + service_count=$(juju status -m $MODEL_NAME | grep kubernetes | grep active | wc -l) echo "$service_count / $total_service_count services active" if [ $service_count -eq $total_service_count ]; then break @@ -350,6 +344,12 @@ EOF } function generate_images_overlay(){ + cat << EOF > /tmp/nbi_registry.yaml +registrypath: ${REGISTRY_URL}opensourcemano/nbi:$TAG +EOF + cat << EOF > /tmp/ng_ui_registry.yaml +registrypath: ${REGISTRY_URL}opensourcemano/ng-ui:$TAG +EOF if [ ! -z "$REGISTRY_USERNAME" ] ; then REGISTRY_CREDENTIALS=$(cat <> /tmp/nbi_registry.yaml + echo password: $REGISTRY_PASSWORD >> /tmp/nbi_registry.yaml + echo username: $REGISTRY_USERNAME >> /tmp/ng_ui_registry.yaml + echo password: $REGISTRY_PASSWORD >> /tmp/ng_ui_registry.yaml fi cat << EOF > /tmp/images-overlay.yaml @@ -370,9 +374,9 @@ applications: ro-k8s: options: image: ${REGISTRY_URL}opensourcemano/ro:$TAG ${REGISTRY_CREDENTIALS} - nbi-k8s: - options: - image: ${REGISTRY_URL}opensourcemano/nbi:$TAG ${REGISTRY_CREDENTIALS} + nbi: + resources: + image: /tmp/nbi_registry.yaml pol-k8s: options: image: ${REGISTRY_URL}opensourcemano/pol:$TAG ${REGISTRY_CREDENTIALS} @@ -380,8 +384,8 @@ applications: options: image: ${REGISTRY_URL}opensourcemano/pla:$TAG ${REGISTRY_CREDENTIALS} ng-ui: - options: - image: ${REGISTRY_URL}opensourcemano/ng-ui:$TAG ${REGISTRY_CREDENTIALS} + resources: + image: /tmp/ng_ui_registry.yaml keystone: options: image: ${REGISTRY_URL}opensourcemano/keystone:$TAG ${REGISTRY_CREDENTIALS} @@ -407,7 +411,7 @@ function install_microstack() { ubuntu1604 ssh-keygen -t rsa -N "" -f ~/.ssh/microstack microstack.openstack keypair create --public-key ~/.ssh/microstack.pub microstack - export OSM_HOSTNAME=`juju status --format json | jq -rc '.applications."nbi-k8s".address'` + export OSM_HOSTNAME=`juju status --format json | jq -rc '.applications."nbi".address'` osm vim-create --name microstack-site \ --user admin \ --password keystone \ @@ -437,7 +441,7 @@ if [ -v MICROSTACK ]; then install_microstack fi -OSM_HOSTNAME=$(juju config nbi-k8s juju-external-hostname):443 +OSM_HOSTNAME=$(juju config nbi site_url | sed "s/http.*\?:\/\///"):443 echo "Your installation is now complete, follow these steps for configuring the osmclient:" echo -- 2.17.1