From 794fa72d51b56931596d9c93330d97be69966e47 Mon Sep 17 00:00:00 2001 From: beierlm Date: Fri, 25 Aug 2023 23:01:16 +0200 Subject: [PATCH] Revert "Remove Prometheus charm" This reverts commit 722b50279283ef22a9f9fab256e416f795427f26. Reason for revert: Grafana-k8s requires Juju 3.1 and we are not ready to change Juju versions at this time Change-Id: Ibc17a6973c981a21152a8f74c8054e42ac21b24f Signed-off-by: Mark Beierl --- devops-stages/stage-test.sh | 9 + installers/charm/prometheus/.gitignore | 30 + installers/charm/prometheus/.jujuignore | 34 + installers/charm/prometheus/.yamllint.yaml | 34 + installers/charm/prometheus/README.md | 23 + installers/charm/prometheus/actions.yaml | 23 + installers/charm/prometheus/charmcraft.yaml | 41 ++ installers/charm/prometheus/config.yaml | 85 +++ installers/charm/prometheus/icon.svg | 50 ++ installers/charm/prometheus/metadata.yaml | 51 ++ .../charm/prometheus/requirements-test.txt | 20 + installers/charm/prometheus/requirements.txt | 25 + installers/charm/prometheus/src/charm.py | 298 ++++++++ installers/charm/prometheus/src/pod_spec.py | 380 +++++++++++ installers/charm/prometheus/tests/__init__.py | 40 ++ .../charm/prometheus/tests/test_charm.py | 111 +++ .../charm/prometheus/tests/test_pod_spec.py | 640 ++++++++++++++++++ installers/charm/prometheus/tox.ini | 126 ++++ 18 files changed, 2020 insertions(+) create mode 100644 installers/charm/prometheus/.gitignore create mode 100644 installers/charm/prometheus/.jujuignore create mode 100644 installers/charm/prometheus/.yamllint.yaml create mode 100644 installers/charm/prometheus/README.md create mode 100644 installers/charm/prometheus/actions.yaml create mode 100644 installers/charm/prometheus/charmcraft.yaml create mode 100644 installers/charm/prometheus/config.yaml create mode 100644 installers/charm/prometheus/icon.svg create mode 100644 installers/charm/prometheus/metadata.yaml create mode 100644 installers/charm/prometheus/requirements-test.txt create mode 100644 installers/charm/prometheus/requirements.txt create mode 100755 installers/charm/prometheus/src/charm.py create mode 100644 installers/charm/prometheus/src/pod_spec.py create mode 100644 installers/charm/prometheus/tests/__init__.py create mode 100644 installers/charm/prometheus/tests/test_charm.py create mode 100644 installers/charm/prometheus/tests/test_pod_spec.py create mode 100644 installers/charm/prometheus/tox.ini diff --git a/devops-stages/stage-test.sh b/devops-stages/stage-test.sh index 693ca479..1e2f913c 100755 --- a/devops-stages/stage-test.sh +++ b/devops-stages/stage-test.sh @@ -21,6 +21,7 @@ CURRENT_DIR=`pwd` # Execute tests for charms CHARM_PATH="./installers/charm" NEW_CHARMS_NAMES="osm-keystone osm-lcm osm-mon osm-nbi osm-ng-ui osm-pol osm-ro vca-integrator-operator" +OLD_CHARMS_NAMES="prometheus grafana" for charm in $NEW_CHARMS_NAMES; do if [ $(git diff --name-only "origin/${GERRIT_BRANCH}" -- "installers/charm/${charm}" | wc -l) -ne 0 ]; then echo "Running tox for ${charm}" @@ -29,6 +30,14 @@ for charm in $NEW_CHARMS_NAMES; do cd "${CURRENT_DIR}" fi done +for charm in $OLD_CHARMS_NAMES; do + if [ $(git diff --name-only "origin/${GERRIT_BRANCH}" -- "installers/charm/${charm}" | wc -l) -ne 0 ]; then + echo "Running tox for ${charm}" + cd "${CHARM_PATH}/${charm}" + TOX_PARALLEL_NO_SPINNER=1 tox --parallel=auto + cd "${CURRENT_DIR}" + fi +done # Execute linting test for OSM helm chart helm lint installers/helm/osm diff --git a/installers/charm/prometheus/.gitignore b/installers/charm/prometheus/.gitignore new file mode 100644 index 00000000..2885df27 --- /dev/null +++ b/installers/charm/prometheus/.gitignore @@ -0,0 +1,30 @@ +# Copyright 2021 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 +## + +venv +.vscode +build +*.charm +.coverage +coverage.xml +.stestr +cover +release \ No newline at end of file diff --git a/installers/charm/prometheus/.jujuignore b/installers/charm/prometheus/.jujuignore new file mode 100644 index 00000000..3ae3e7dc --- /dev/null +++ b/installers/charm/prometheus/.jujuignore @@ -0,0 +1,34 @@ +# Copyright 2021 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 +## + +venv +.vscode +build +*.charm +.coverage +coverage.xml +.gitignore +.stestr +cover +release +tests/ +requirements* +tox.ini diff --git a/installers/charm/prometheus/.yamllint.yaml b/installers/charm/prometheus/.yamllint.yaml new file mode 100644 index 00000000..d71fb69f --- /dev/null +++ b/installers/charm/prometheus/.yamllint.yaml @@ -0,0 +1,34 @@ +# Copyright 2021 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 +## + +--- +extends: default + +yaml-files: + - "*.yaml" + - "*.yml" + - ".yamllint" +ignore: | + .tox + cover/ + build/ + venv + release/ diff --git a/installers/charm/prometheus/README.md b/installers/charm/prometheus/README.md new file mode 100644 index 00000000..0486c0db --- /dev/null +++ b/installers/charm/prometheus/README.md @@ -0,0 +1,23 @@ + + +# Prometheus operator Charm for Kubernetes + +## Requirements diff --git a/installers/charm/prometheus/actions.yaml b/installers/charm/prometheus/actions.yaml new file mode 100644 index 00000000..e41f3df0 --- /dev/null +++ b/installers/charm/prometheus/actions.yaml @@ -0,0 +1,23 @@ +# Copyright 2021 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 +## + +backup: + description: "Do a mongodb backup" diff --git a/installers/charm/prometheus/charmcraft.yaml b/installers/charm/prometheus/charmcraft.yaml new file mode 100644 index 00000000..87d04635 --- /dev/null +++ b/installers/charm/prometheus/charmcraft.yaml @@ -0,0 +1,41 @@ +# Copyright 2021 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 +## + +type: charm +bases: + - build-on: + - name: ubuntu + channel: "20.04" + architectures: ["amd64"] + run-on: + - name: ubuntu + channel: "20.04" + architectures: + - amd64 + - aarch64 + - arm64 +parts: + charm: + build-packages: + - cargo + - git + - libffi-dev + - rustc diff --git a/installers/charm/prometheus/config.yaml b/installers/charm/prometheus/config.yaml new file mode 100644 index 00000000..b25eabae --- /dev/null +++ b/installers/charm/prometheus/config.yaml @@ -0,0 +1,85 @@ +# Copyright 2021 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 +## + +options: + web-subpath: + description: Subpath for accessing Prometheus + type: string + default: / + default-target: + description: Default target to be added in Prometheus + type: string + default: "" + max_file_size: + type: int + 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_class: + type: string + description: | + Ingress class name. This is useful for selecting the ingress to be used + in case there are multiple ingresses in the underlying k8s clusters. + ingress_whitelist_source_range: + type: string + description: | + A comma-separated list of CIDRs to store in the + ingress.kubernetes.io/whitelist-source-range annotation. + + This can be used to lock down access to + Keystone based on source IP address. + default: "" + tls_secret_name: + type: string + description: TLS Secret name + default: "" + site_url: + type: string + description: Ingress URL + default: "" + cluster_issuer: + type: string + description: Name of the cluster issuer for TLS certificates + default: "" + enable_web_admin_api: + type: boolean + description: Boolean to enable the web admin api + default: false + image_pull_policy: + type: string + description: | + ImagePullPolicy configuration for the pod. + Possible values: always, ifnotpresent, never + default: always + security_context: + description: Enables the security context of the pods + type: boolean + default: false + web_config_username: + type: string + default: admin + description: Username to access the Prometheus Web Interface + web_config_password: + type: string + default: admin + description: Password to access the Prometheus Web Interface diff --git a/installers/charm/prometheus/icon.svg b/installers/charm/prometheus/icon.svg new file mode 100644 index 00000000..5c51f66d --- /dev/null +++ b/installers/charm/prometheus/icon.svg @@ -0,0 +1,50 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/installers/charm/prometheus/metadata.yaml b/installers/charm/prometheus/metadata.yaml new file mode 100644 index 00000000..932ccc21 --- /dev/null +++ b/installers/charm/prometheus/metadata.yaml @@ -0,0 +1,51 @@ +# Copyright 2021 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 +## + +name: osm-prometheus +summary: OSM Prometheus +description: | + A CAAS charm to deploy OSM's Prometheus. +series: + - kubernetes +tags: + - kubernetes + - osm + - prometheus +min-juju-version: 2.8.0 +deployment: + type: stateful + service: cluster +resources: + backup-image: + type: oci-image + description: Container image to run backup actions + upstream-source: "ed1000/prometheus-backup:latest" + image: + type: oci-image + description: OSM docker image for Prometheus + upstream-source: "ubuntu/prometheus:latest" +provides: + prometheus: + interface: prometheus +storage: + data: + type: filesystem + location: /prometheus diff --git a/installers/charm/prometheus/requirements-test.txt b/installers/charm/prometheus/requirements-test.txt new file mode 100644 index 00000000..cf61dd4e --- /dev/null +++ b/installers/charm/prometheus/requirements-test.txt @@ -0,0 +1,20 @@ +# Copyright 2021 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 +mock==4.0.3 diff --git a/installers/charm/prometheus/requirements.txt b/installers/charm/prometheus/requirements.txt new file mode 100644 index 00000000..db13e518 --- /dev/null +++ b/installers/charm/prometheus/requirements.txt @@ -0,0 +1,25 @@ +# Copyright 2021 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 +## + +git+https://github.com/charmed-osm/ops-lib-charmed-osm/@master +requests +urllib3>1.25.9 +bcrypt diff --git a/installers/charm/prometheus/src/charm.py b/installers/charm/prometheus/src/charm.py new file mode 100755 index 00000000..af39a13a --- /dev/null +++ b/installers/charm/prometheus/src/charm.py @@ -0,0 +1,298 @@ +#!/usr/bin/env python3 +# Copyright 2021 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 +## + +# pylint: disable=E0213 + +import base64 +from ipaddress import ip_network +import logging +from typing import NoReturn, Optional +from urllib.parse import urlparse + +import bcrypt +from oci_image import OCIImageResource +from ops.framework import EventBase +from ops.main import main +from opslib.osm.charm import CharmedOsmBase +from opslib.osm.interfaces.prometheus import PrometheusServer +from opslib.osm.pod import ( + ContainerV3Builder, + FilesV3Builder, + IngressResourceV3Builder, + PodSpecV3Builder, +) +from opslib.osm.validator import ( + ModelValidator, + validator, +) +import requests + + +logger = logging.getLogger(__name__) + +PORT = 9090 + + +class ConfigModel(ModelValidator): + web_subpath: str + default_target: str + max_file_size: int + site_url: Optional[str] + cluster_issuer: Optional[str] + ingress_class: Optional[str] + ingress_whitelist_source_range: Optional[str] + tls_secret_name: Optional[str] + enable_web_admin_api: bool + image_pull_policy: str + security_context: bool + web_config_username: str + web_config_password: str + + @validator("web_subpath") + def validate_web_subpath(cls, v): + if len(v) < 1: + raise ValueError("web-subpath must be a non-empty string") + return v + + @validator("max_file_size") + def validate_max_file_size(cls, v): + if v < 0: + raise ValueError("value must be equal or greater than 0") + return v + + @validator("site_url") + def validate_site_url(cls, v): + if v: + parsed = urlparse(v) + if not parsed.scheme.startswith("http"): + raise ValueError("value must start with http") + return v + + @validator("ingress_whitelist_source_range") + def validate_ingress_whitelist_source_range(cls, v): + if v: + ip_network(v) + return v + + @validator("image_pull_policy") + def validate_image_pull_policy(cls, v): + values = { + "always": "Always", + "ifnotpresent": "IfNotPresent", + "never": "Never", + } + v = v.lower() + if v not in values.keys(): + raise ValueError("value must be always, ifnotpresent or never") + return values[v] + + +class PrometheusCharm(CharmedOsmBase): + + """Prometheus Charm.""" + + def __init__(self, *args) -> NoReturn: + """Prometheus Charm constructor.""" + super().__init__(*args, oci_image="image") + + # Registering provided relation events + self.prometheus = PrometheusServer(self, "prometheus") + self.framework.observe( + self.on.prometheus_relation_joined, # pylint: disable=E1101 + self._publish_prometheus_info, + ) + + # Registering actions + self.framework.observe( + self.on.backup_action, # pylint: disable=E1101 + self._on_backup_action, + ) + + def _publish_prometheus_info(self, event: EventBase) -> NoReturn: + config = ConfigModel(**dict(self.config)) + self.prometheus.publish_info( + self.app.name, + PORT, + user=config.web_config_username, + password=config.web_config_password, + ) + + def _on_backup_action(self, event: EventBase) -> NoReturn: + url = f"http://{self.model.app.name}:{PORT}/api/v1/admin/tsdb/snapshot" + result = requests.post(url) + + if result.status_code == 200: + event.set_results({"backup-name": result.json()["name"]}) + else: + event.fail(f"status-code: {result.status_code}") + + def _build_config_file(self, config: ConfigModel): + files_builder = FilesV3Builder() + files_builder.add_file( + "prometheus.yml", + ( + "global:\n" + " scrape_interval: 15s\n" + " evaluation_interval: 15s\n" + "alerting:\n" + " alertmanagers:\n" + " - static_configs:\n" + " - targets:\n" + "rule_files:\n" + "scrape_configs:\n" + " - job_name: 'prometheus'\n" + " static_configs:\n" + f" - targets: [{config.default_target}]\n" + ), + ) + return files_builder.build() + + def _build_webconfig_file(self): + files_builder = FilesV3Builder() + files_builder.add_file("web.yml", "web-config-file", secret=True) + return files_builder.build() + + def build_pod_spec(self, image_info): + # Validate config + config = ConfigModel(**dict(self.config)) + # Create Builder for the PodSpec + pod_spec_builder = PodSpecV3Builder( + enable_security_context=config.security_context + ) + + # Build Backup Container + backup_image = OCIImageResource(self, "backup-image") + backup_image_info = backup_image.fetch() + backup_container_builder = ContainerV3Builder("prom-backup", backup_image_info) + backup_container = backup_container_builder.build() + + # Add backup container to pod spec + pod_spec_builder.add_container(backup_container) + + # Add pod secrets + prometheus_secret_name = f"{self.app.name}-secret" + pod_spec_builder.add_secret( + prometheus_secret_name, + { + "web-config-file": ( + "basic_auth_users:\n" + f" {config.web_config_username}: {self._hash_password(config.web_config_password)}\n" + ) + }, + ) + + # Build Container + container_builder = ContainerV3Builder( + self.app.name, + image_info, + config.image_pull_policy, + run_as_non_root=config.security_context, + ) + container_builder.add_port(name=self.app.name, port=PORT) + token = self._base64_encode( + f"{config.web_config_username}:{config.web_config_password}" + ) + container_builder.add_http_readiness_probe( + "/-/ready", + PORT, + initial_delay_seconds=10, + timeout_seconds=30, + http_headers=[("Authorization", f"Basic {token}")], + ) + container_builder.add_http_liveness_probe( + "/-/healthy", + PORT, + initial_delay_seconds=30, + period_seconds=30, + http_headers=[("Authorization", f"Basic {token}")], + ) + command = [ + "/bin/prometheus", + "--config.file=/etc/prometheus/prometheus.yml", + "--web.config.file=/etc/prometheus/web-config/web.yml", + "--storage.tsdb.path=/prometheus", + "--web.console.libraries=/usr/share/prometheus/console_libraries", + "--web.console.templates=/usr/share/prometheus/consoles", + f"--web.route-prefix={config.web_subpath}", + f"--web.external-url=http://localhost:{PORT}{config.web_subpath}", + ] + if config.enable_web_admin_api: + command.append("--web.enable-admin-api") + container_builder.add_command(command) + container_builder.add_volume_config( + "config", "/etc/prometheus", self._build_config_file(config) + ) + container_builder.add_volume_config( + "web-config", + "/etc/prometheus/web-config", + self._build_webconfig_file(), + secret_name=prometheus_secret_name, + ) + container = container_builder.build() + # Add container to pod spec + pod_spec_builder.add_container(container) + # Add ingress resources to pod spec if site url exists + if config.site_url: + parsed = urlparse(config.site_url) + annotations = { + "nginx.ingress.kubernetes.io/proxy-body-size": "{}".format( + str(config.max_file_size) + "m" + if config.max_file_size > 0 + else config.max_file_size + ) + } + if config.ingress_class: + annotations["kubernetes.io/ingress.class"] = config.ingress_class + ingress_resource_builder = IngressResourceV3Builder( + f"{self.app.name}-ingress", annotations + ) + + if config.ingress_whitelist_source_range: + annotations[ + "nginx.ingress.kubernetes.io/whitelist-source-range" + ] = config.ingress_whitelist_source_range + + if config.cluster_issuer: + annotations["cert-manager.io/cluster-issuer"] = config.cluster_issuer + + if parsed.scheme == "https": + ingress_resource_builder.add_tls( + [parsed.hostname], config.tls_secret_name + ) + else: + annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false" + + ingress_resource_builder.add_rule(parsed.hostname, self.app.name, PORT) + ingress_resource = ingress_resource_builder.build() + pod_spec_builder.add_ingress_resource(ingress_resource) + return pod_spec_builder.build() + + def _hash_password(self, password): + hashed_password = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()) + return hashed_password.decode() + + def _base64_encode(self, phrase: str) -> str: + return base64.b64encode(phrase.encode("utf-8")).decode("utf-8") + + +if __name__ == "__main__": + main(PrometheusCharm) diff --git a/installers/charm/prometheus/src/pod_spec.py b/installers/charm/prometheus/src/pod_spec.py new file mode 100644 index 00000000..202114ee --- /dev/null +++ b/installers/charm/prometheus/src/pod_spec.py @@ -0,0 +1,380 @@ +#!/usr/bin/env python3 +# Copyright 2021 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 +## + +from ipaddress import ip_network +import logging +from typing import Any, Dict, List +from urllib.parse import urlparse + +logger = logging.getLogger(__name__) + + +def _validate_max_file_size(max_file_size: int, site_url: str) -> bool: + """Validate max_file_size. + + Args: + max_file_size (int): maximum file size allowed. + site_url (str): endpoint url. + + Returns: + bool: True if valid, false otherwise. + """ + if not site_url: + return True + + parsed = urlparse(site_url) + + if not parsed.scheme.startswith("http"): + return True + + if max_file_size is None: + return False + + return max_file_size >= 0 + + +def _validate_ip_network(network: str) -> bool: + """Validate IP network. + + Args: + network (str): IP network range. + + Returns: + bool: True if valid, false otherwise. + """ + if not network: + return True + + try: + ip_network(network) + except ValueError: + return False + + return True + + +def _validate_data(config_data: Dict[str, Any], relation_data: Dict[str, Any]) -> bool: + """Validates passed information. + + Args: + config_data (Dict[str, Any]): configuration information. + relation_data (Dict[str, Any]): relation information + + Raises: + ValueError: when config and/or relation data is not valid. + """ + config_validators = { + "web_subpath": lambda value, _: isinstance(value, str) and len(value) > 0, + "default_target": lambda value, _: isinstance(value, str), + "site_url": lambda value, _: isinstance(value, str) + if value is not None + else True, + "max_file_size": lambda value, values: _validate_max_file_size( + value, values.get("site_url") + ), + "ingress_whitelist_source_range": lambda value, _: _validate_ip_network(value), + "tls_secret_name": lambda value, _: isinstance(value, str) + if value is not None + else True, + "enable_web_admin_api": lambda value, _: isinstance(value, bool), + } + relation_validators = {} + problems = [] + + for key, validator in config_validators.items(): + valid = validator(config_data.get(key), config_data) + + if not valid: + problems.append(key) + + for key, validator in relation_validators.items(): + valid = validator(relation_data.get(key), relation_data) + + if not valid: + problems.append(key) + + if len(problems) > 0: + raise ValueError("Errors found in: {}".format(", ".join(problems))) + + return True + + +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": "prometheus", "containerPort": port, "protocol": "TCP"}] + + +def _make_pod_envconfig( + config: Dict[str, Any], relation_state: Dict[str, Any] +) -> Dict[str, Any]: + """Generate pod environment configuration. + + Args: + config (Dict[str, Any]): configuration information. + relation_state (Dict[str, Any]): relation state information. + + Returns: + Dict[str, Any]: pod environment configuration. + """ + envconfig = {} + + return envconfig + + +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_pod_files(config: Dict[str, Any]) -> List[Dict[str, Any]]: + """Generating ConfigMap information + + Args: + config (Dict[str, Any]): configuration information. + + Returns: + List[Dict[str, Any]]: ConfigMap information. + """ + files = [ + { + "name": "config", + "mountPath": "/etc/prometheus", + "files": [ + { + "path": "prometheus.yml", + "content": ( + "global:\n" + " scrape_interval: 15s\n" + " evaluation_interval: 15s\n" + "alerting:\n" + " alertmanagers:\n" + " - static_configs:\n" + " - targets:\n" + "rule_files:\n" + "scrape_configs:\n" + " - job_name: 'prometheus'\n" + " static_configs:\n" + " - targets: [{}]\n".format(config["default_target"]) + ), + } + ], + } + ] + + return files + + +def _make_readiness_probe(port: int) -> Dict[str, Any]: + """Generate readiness probe. + + Args: + port (int): service port. + + Returns: + Dict[str, Any]: readiness probe. + """ + return { + "httpGet": { + "path": "/-/ready", + "port": port, + }, + "initialDelaySeconds": 10, + "timeoutSeconds": 30, + } + + +def _make_liveness_probe(port: int) -> Dict[str, Any]: + """Generate liveness probe. + + Args: + port (int): service port. + + Returns: + Dict[str, Any]: liveness probe. + """ + return { + "httpGet": { + "path": "/-/healthy", + "port": port, + }, + "initialDelaySeconds": 30, + "periodSeconds": 30, + } + + +def _make_pod_command(config: Dict[str, Any], port: int) -> List[str]: + """Generate the startup command. + + Args: + config (Dict[str, Any]): Configuration information. + port (int): port. + + Returns: + List[str]: command to startup the process. + """ + command = [ + "/bin/prometheus", + "--config.file=/etc/prometheus/prometheus.yml", + "--storage.tsdb.path=/prometheus", + "--web.console.libraries=/usr/share/prometheus/console_libraries", + "--web.console.templates=/usr/share/prometheus/consoles", + "--web.route-prefix={}".format(config.get("web_subpath")), + "--web.external-url=http://localhost:{}{}".format( + port, config.get("web_subpath") + ), + ] + if config.get("enable_web_admin_api"): + command.append("--web.enable-admin-api") + return command + + +def make_pod_spec( + image_info: Dict[str, str], + config: Dict[str, Any], + relation_state: Dict[str, Any], + app_name: str = "prometheus", + port: int = 9090, +) -> 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 "ro". + port (int, optional): Port for the container. Defaults to 9090. + + Returns: + Dict[str, Any]: Pod spec dictionary for the charm. + """ + if not image_info: + return None + + _validate_data(config, relation_state) + + ports = _make_pod_ports(port) + env_config = _make_pod_envconfig(config, relation_state) + files = _make_pod_files(config) + readiness_probe = _make_readiness_probe(port) + liveness_probe = _make_liveness_probe(port) + ingress_resources = _make_pod_ingress_resources(config, app_name, port) + command = _make_pod_command(config, port) + + return { + "version": 3, + "containers": [ + { + "name": app_name, + "imageDetails": image_info, + "imagePullPolicy": "Always", + "ports": ports, + "envConfig": env_config, + "volumeConfig": files, + "command": command, + "kubernetes": { + "readinessProbe": readiness_probe, + "livenessProbe": liveness_probe, + }, + } + ], + "kubernetesResources": { + "ingressResources": ingress_resources or [], + }, + } diff --git a/installers/charm/prometheus/tests/__init__.py b/installers/charm/prometheus/tests/__init__.py new file mode 100644 index 00000000..446d5cee --- /dev/null +++ b/installers/charm/prometheus/tests/__init__.py @@ -0,0 +1,40 @@ +#!/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 +## + +"""Init mocking for unit tests.""" + +import sys + + +import mock + + +class OCIImageResourceErrorMock(Exception): + pass + + +sys.path.append("src") + +oci_image = mock.MagicMock() +oci_image.OCIImageResourceError = OCIImageResourceErrorMock +sys.modules["oci_image"] = oci_image +sys.modules["oci_image"].OCIImageResource().fetch.return_value = {} diff --git a/installers/charm/prometheus/tests/test_charm.py b/installers/charm/prometheus/tests/test_charm.py new file mode 100644 index 00000000..965400a4 --- /dev/null +++ b/installers/charm/prometheus/tests/test_charm.py @@ -0,0 +1,111 @@ +#!/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 sys +from typing import NoReturn +import unittest + +from charm import PrometheusCharm +from ops.model import ActiveStatus +from ops.testing import Harness + + +class TestCharm(unittest.TestCase): + """Prometheus Charm unit tests.""" + + def setUp(self) -> NoReturn: + """Test setup""" + self.image_info = sys.modules["oci_image"].OCIImageResource().fetch() + self.harness = Harness(PrometheusCharm) + self.harness.set_leader(is_leader=True) + self.harness.begin() + self.config = { + "web-subpath": "/", + "default-target": "", + "max_file_size": 0, + "ingress_whitelist_source_range": "", + "tls_secret_name": "", + "site_url": "https://prometheus.192.168.100.100.nip.io", + "cluster_issuer": "vault-issuer", + "enable_web_admin_api": False, + "web_config_username": "admin", + "web_config_password": "1234", + } + self.harness.update_config(self.config) + + def test_config_changed( + self, + ) -> NoReturn: + """Test ingress resources without HTTP.""" + + self.harness.charm.on.config_changed.emit() + + # Assertions + self.assertIsInstance(self.harness.charm.unit.status, ActiveStatus) + + def test_config_changed_non_leader( + self, + ) -> NoReturn: + """Test ingress resources without HTTP.""" + self.harness.set_leader(is_leader=False) + self.harness.charm.on.config_changed.emit() + + # Assertions + self.assertIsInstance(self.harness.charm.unit.status, ActiveStatus) + + def test_publish_prometheus_info( + self, + ) -> NoReturn: + """Test to see if prometheus relation is updated.""" + expected_result = { + "hostname": self.harness.charm.app.name, + "port": "9090", + "user": "admin", + "password": "1234", + } + + relation_id = self.harness.add_relation("prometheus", "mon") + self.harness.add_relation_unit(relation_id, "mon/0") + relation_data = self.harness.get_relation_data( + relation_id, self.harness.charm.app.name + ) + + self.assertDictEqual(expected_result, relation_data) + + def test_publish_prometheus_info_non_leader( + self, + ) -> NoReturn: + """Test to see if prometheus relation is updated.""" + expected_result = {} + + self.harness.set_leader(is_leader=False) + relation_id = self.harness.add_relation("prometheus", "mon") + self.harness.add_relation_unit(relation_id, "mon/0") + relation_data = self.harness.get_relation_data( + relation_id, self.harness.charm.app.name + ) + + self.assertDictEqual(expected_result, relation_data) + + +if __name__ == "__main__": + unittest.main() diff --git a/installers/charm/prometheus/tests/test_pod_spec.py b/installers/charm/prometheus/tests/test_pod_spec.py new file mode 100644 index 00000000..1adbae64 --- /dev/null +++ b/installers/charm/prometheus/tests/test_pod_spec.py @@ -0,0 +1,640 @@ +#!/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 +## + +from typing import NoReturn +import unittest + +import pod_spec + + +class TestPodSpec(unittest.TestCase): + """Pod spec unit tests.""" + + def test_make_pod_ports(self) -> NoReturn: + """Testing make pod ports.""" + port = 9090 + + expected_result = [ + { + "name": "prometheus", + "containerPort": port, + "protocol": "TCP", + } + ] + + pod_ports = pod_spec._make_pod_ports(port) + + self.assertListEqual(expected_result, pod_ports) + + def test_make_pod_envconfig(self) -> NoReturn: + """Testing make pod envconfig.""" + config = {} + relation_state = {} + + expected_result = {} + + pod_envconfig = pod_spec._make_pod_envconfig(config, relation_state) + + self.assertDictEqual(expected_result, pod_envconfig) + + def test_make_pod_ingress_resources_without_site_url(self) -> NoReturn: + """Testing make pod ingress resources without site_url.""" + config = {"site_url": ""} + app_name = "prometheus" + port = 9090 + + pod_ingress_resources = pod_spec._make_pod_ingress_resources( + config, app_name, port + ) + + self.assertIsNone(pod_ingress_resources) + + def test_make_pod_ingress_resources(self) -> NoReturn: + """Testing make pod ingress resources.""" + config = { + "site_url": "http://prometheus", + "max_file_size": 0, + "ingress_whitelist_source_range": "", + } + app_name = "prometheus" + port = 9090 + + expected_result = [ + { + "name": f"{app_name}-ingress", + "annotations": { + "nginx.ingress.kubernetes.io/proxy-body-size": f"{config['max_file_size']}", + "nginx.ingress.kubernetes.io/ssl-redirect": "false", + }, + "spec": { + "rules": [ + { + "host": app_name, + "http": { + "paths": [ + { + "path": "/", + "backend": { + "serviceName": app_name, + "servicePort": port, + }, + } + ] + }, + } + ] + }, + } + ] + + pod_ingress_resources = pod_spec._make_pod_ingress_resources( + config, app_name, port + ) + + self.assertListEqual(expected_result, pod_ingress_resources) + + def test_make_pod_ingress_resources_with_whitelist_source_range(self) -> NoReturn: + """Testing make pod ingress resources with whitelist_source_range.""" + config = { + "site_url": "http://prometheus", + "max_file_size": 0, + "ingress_whitelist_source_range": "0.0.0.0/0", + } + app_name = "prometheus" + port = 9090 + + expected_result = [ + { + "name": f"{app_name}-ingress", + "annotations": { + "nginx.ingress.kubernetes.io/proxy-body-size": f"{config['max_file_size']}", + "nginx.ingress.kubernetes.io/ssl-redirect": "false", + "nginx.ingress.kubernetes.io/whitelist-source-range": config[ + "ingress_whitelist_source_range" + ], + }, + "spec": { + "rules": [ + { + "host": app_name, + "http": { + "paths": [ + { + "path": "/", + "backend": { + "serviceName": app_name, + "servicePort": port, + }, + } + ] + }, + } + ] + }, + } + ] + + pod_ingress_resources = pod_spec._make_pod_ingress_resources( + config, app_name, port + ) + + self.assertListEqual(expected_result, pod_ingress_resources) + + def test_make_pod_ingress_resources_with_https(self) -> NoReturn: + """Testing make pod ingress resources with HTTPs.""" + config = { + "site_url": "https://prometheus", + "max_file_size": 0, + "ingress_whitelist_source_range": "", + "tls_secret_name": "", + } + app_name = "prometheus" + port = 9090 + + expected_result = [ + { + "name": f"{app_name}-ingress", + "annotations": { + "nginx.ingress.kubernetes.io/proxy-body-size": f"{config['max_file_size']}", + }, + "spec": { + "rules": [ + { + "host": app_name, + "http": { + "paths": [ + { + "path": "/", + "backend": { + "serviceName": app_name, + "servicePort": port, + }, + } + ] + }, + } + ], + "tls": [{"hosts": [app_name]}], + }, + } + ] + + pod_ingress_resources = pod_spec._make_pod_ingress_resources( + config, app_name, port + ) + + self.assertListEqual(expected_result, pod_ingress_resources) + + def test_make_pod_ingress_resources_with_https_tls_secret_name(self) -> NoReturn: + """Testing make pod ingress resources with HTTPs and TLS secret name.""" + config = { + "site_url": "https://prometheus", + "max_file_size": 0, + "ingress_whitelist_source_range": "", + "tls_secret_name": "secret_name", + } + app_name = "prometheus" + port = 9090 + + expected_result = [ + { + "name": f"{app_name}-ingress", + "annotations": { + "nginx.ingress.kubernetes.io/proxy-body-size": f"{config['max_file_size']}", + }, + "spec": { + "rules": [ + { + "host": app_name, + "http": { + "paths": [ + { + "path": "/", + "backend": { + "serviceName": app_name, + "servicePort": port, + }, + } + ] + }, + } + ], + "tls": [ + {"hosts": [app_name], "secretName": config["tls_secret_name"]} + ], + }, + } + ] + + pod_ingress_resources = pod_spec._make_pod_ingress_resources( + config, app_name, port + ) + + self.assertListEqual(expected_result, pod_ingress_resources) + + def test_make_pod_files(self) -> NoReturn: + """Testing make pod files.""" + config = { + "web_subpath": "/", + "default_target": "", + "site_url": "", + } + + expected_result = [ + { + "name": "config", + "mountPath": "/etc/prometheus", + "files": [ + { + "path": "prometheus.yml", + "content": ( + "global:\n" + " scrape_interval: 15s\n" + " evaluation_interval: 15s\n" + "alerting:\n" + " alertmanagers:\n" + " - static_configs:\n" + " - targets:\n" + "rule_files:\n" + "scrape_configs:\n" + " - job_name: 'prometheus'\n" + " static_configs:\n" + " - targets: [{}]\n".format(config["default_target"]) + ), + } + ], + } + ] + + pod_envconfig = pod_spec._make_pod_files(config) + self.assertListEqual(expected_result, pod_envconfig) + + def test_make_readiness_probe(self) -> NoReturn: + """Testing make readiness probe.""" + port = 9090 + + expected_result = { + "httpGet": { + "path": "/-/ready", + "port": port, + }, + "initialDelaySeconds": 10, + "timeoutSeconds": 30, + } + + readiness_probe = pod_spec._make_readiness_probe(port) + + self.assertDictEqual(expected_result, readiness_probe) + + def test_make_liveness_probe(self) -> NoReturn: + """Testing make liveness probe.""" + port = 9090 + + expected_result = { + "httpGet": { + "path": "/-/healthy", + "port": port, + }, + "initialDelaySeconds": 30, + "periodSeconds": 30, + } + + liveness_probe = pod_spec._make_liveness_probe(port) + + self.assertDictEqual(expected_result, liveness_probe) + + def test_make_pod_command(self) -> NoReturn: + """Testing make pod command.""" + port = 9090 + config = { + "web_subpath": "/", + "default_target": "", + "site_url": "", + } + + expected_result = [ + "/bin/prometheus", + "--config.file=/etc/prometheus/prometheus.yml", + "--storage.tsdb.path=/prometheus", + "--web.console.libraries=/usr/share/prometheus/console_libraries", + "--web.console.templates=/usr/share/prometheus/consoles", + "--web.route-prefix={}".format(config.get("web_subpath")), + "--web.external-url=http://localhost:{}{}".format( + port, config.get("web_subpath") + ), + ] + + pod_envconfig = pod_spec._make_pod_command(config, port) + + self.assertListEqual(expected_result, pod_envconfig) + + def test_make_pod_command_with_web_admin_api_enabled(self) -> NoReturn: + """Testing make pod command.""" + port = 9090 + config = { + "web_subpath": "/", + "default_target": "", + "site_url": "", + "enable_web_admin_api": True, + } + + expected_result = [ + "/bin/prometheus", + "--config.file=/etc/prometheus/prometheus.yml", + "--storage.tsdb.path=/prometheus", + "--web.console.libraries=/usr/share/prometheus/console_libraries", + "--web.console.templates=/usr/share/prometheus/consoles", + "--web.route-prefix={}".format(config.get("web_subpath")), + "--web.external-url=http://localhost:{}{}".format( + port, config.get("web_subpath") + ), + "--web.enable-admin-api", + ] + + pod_envconfig = pod_spec._make_pod_command(config, port) + + self.assertListEqual(expected_result, pod_envconfig) + + def test_make_pod_spec(self) -> NoReturn: + """Testing make pod spec.""" + image_info = {"upstream-source": "ubuntu/prometheus:latest"} + config = { + "web_subpath": "/", + "default_target": "", + "site_url": "", + "enable_web_admin_api": False, + } + relation_state = {} + app_name = "prometheus" + port = 9090 + + expected_result = { + "version": 3, + "containers": [ + { + "name": app_name, + "imageDetails": image_info, + "imagePullPolicy": "Always", + "ports": [ + { + "name": app_name, + "containerPort": port, + "protocol": "TCP", + } + ], + "envConfig": {}, + "volumeConfig": [ + { + "name": "config", + "mountPath": "/etc/prometheus", + "files": [ + { + "path": "prometheus.yml", + "content": ( + "global:\n" + " scrape_interval: 15s\n" + " evaluation_interval: 15s\n" + "alerting:\n" + " alertmanagers:\n" + " - static_configs:\n" + " - targets:\n" + "rule_files:\n" + "scrape_configs:\n" + " - job_name: 'prometheus'\n" + " static_configs:\n" + " - targets: [{}]\n".format( + config.get("default_target") + ) + ), + } + ], + } + ], + "command": [ + "/bin/prometheus", + "--config.file=/etc/prometheus/prometheus.yml", + "--storage.tsdb.path=/prometheus", + "--web.console.libraries=/usr/share/prometheus/console_libraries", + "--web.console.templates=/usr/share/prometheus/consoles", + "--web.route-prefix={}".format(config.get("web_subpath")), + "--web.external-url=http://localhost:{}{}".format( + port, config.get("web_subpath") + ), + ], + "kubernetes": { + "readinessProbe": { + "httpGet": { + "path": "/-/ready", + "port": port, + }, + "initialDelaySeconds": 10, + "timeoutSeconds": 30, + }, + "livenessProbe": { + "httpGet": { + "path": "/-/healthy", + "port": port, + }, + "initialDelaySeconds": 30, + "periodSeconds": 30, + }, + }, + } + ], + "kubernetesResources": {"ingressResources": []}, + } + + spec = pod_spec.make_pod_spec( + image_info, config, relation_state, app_name, port + ) + + self.assertDictEqual(expected_result, spec) + + def test_make_pod_spec_with_ingress(self) -> NoReturn: + """Testing make pod spec.""" + image_info = {"upstream-source": "ubuntu/prometheus:latest"} + config = { + "web_subpath": "/", + "default_target": "", + "site_url": "https://prometheus", + "tls_secret_name": "prometheus", + "max_file_size": 0, + "ingress_whitelist_source_range": "0.0.0.0/0", + "enable_web_admin_api": False, + } + relation_state = {} + app_name = "prometheus" + port = 9090 + + expected_result = { + "version": 3, + "containers": [ + { + "name": app_name, + "imageDetails": image_info, + "imagePullPolicy": "Always", + "ports": [ + { + "name": app_name, + "containerPort": port, + "protocol": "TCP", + } + ], + "envConfig": {}, + "volumeConfig": [ + { + "name": "config", + "mountPath": "/etc/prometheus", + "files": [ + { + "path": "prometheus.yml", + "content": ( + "global:\n" + " scrape_interval: 15s\n" + " evaluation_interval: 15s\n" + "alerting:\n" + " alertmanagers:\n" + " - static_configs:\n" + " - targets:\n" + "rule_files:\n" + "scrape_configs:\n" + " - job_name: 'prometheus'\n" + " static_configs:\n" + " - targets: [{}]\n".format( + config.get("default_target") + ) + ), + } + ], + } + ], + "command": [ + "/bin/prometheus", + "--config.file=/etc/prometheus/prometheus.yml", + "--storage.tsdb.path=/prometheus", + "--web.console.libraries=/usr/share/prometheus/console_libraries", + "--web.console.templates=/usr/share/prometheus/consoles", + "--web.route-prefix={}".format(config.get("web_subpath")), + "--web.external-url=http://localhost:{}{}".format( + port, config.get("web_subpath") + ), + ], + "kubernetes": { + "readinessProbe": { + "httpGet": { + "path": "/-/ready", + "port": port, + }, + "initialDelaySeconds": 10, + "timeoutSeconds": 30, + }, + "livenessProbe": { + "httpGet": { + "path": "/-/healthy", + "port": port, + }, + "initialDelaySeconds": 30, + "periodSeconds": 30, + }, + }, + } + ], + "kubernetesResources": { + "ingressResources": [ + { + "name": "{}-ingress".format(app_name), + "annotations": { + "nginx.ingress.kubernetes.io/proxy-body-size": str( + config.get("max_file_size") + ), + "nginx.ingress.kubernetes.io/whitelist-source-range": config.get( + "ingress_whitelist_source_range" + ), + }, + "spec": { + "rules": [ + { + "host": app_name, + "http": { + "paths": [ + { + "path": "/", + "backend": { + "serviceName": app_name, + "servicePort": port, + }, + } + ] + }, + } + ], + "tls": [ + { + "hosts": [app_name], + "secretName": config.get("tls_secret_name"), + } + ], + }, + } + ], + }, + } + + spec = pod_spec.make_pod_spec( + image_info, config, relation_state, app_name, port + ) + + self.assertDictEqual(expected_result, spec) + + def test_make_pod_spec_without_image_info(self) -> NoReturn: + """Testing make pod spec without image_info.""" + image_info = None + config = { + "web_subpath": "/", + "default_target": "", + "site_url": "", + "enable_web_admin_api": False, + } + relation_state = {} + app_name = "prometheus" + port = 9090 + + spec = pod_spec.make_pod_spec( + image_info, config, relation_state, app_name, port + ) + + self.assertIsNone(spec) + + def test_make_pod_spec_without_config(self) -> NoReturn: + """Testing make pod spec without config.""" + image_info = {"upstream-source": "ubuntu/prometheus:latest"} + config = {} + relation_state = {} + app_name = "prometheus" + port = 9090 + + with self.assertRaises(ValueError): + pod_spec.make_pod_spec(image_info, config, relation_state, app_name, port) + + +if __name__ == "__main__": + unittest.main() diff --git a/installers/charm/prometheus/tox.ini b/installers/charm/prometheus/tox.ini new file mode 100644 index 00000000..4c7970df --- /dev/null +++ b/installers/charm/prometheus/tox.ini @@ -0,0 +1,126 @@ +# Copyright 2021 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 +## +####################################################################################### + +[tox] +envlist = black, cover, flake8, pylint, yamllint, safety +skipsdist = true + +[tox:jenkins] +toxworkdir = /tmp/.tox + +[testenv] +basepython = python3.8 +setenv = VIRTUAL_ENV={envdir} + PYTHONDONTWRITEBYTECODE = 1 +deps = -r{toxinidir}/requirements.txt + + +####################################################################################### +[testenv:black] +deps = black +commands = + black --check --diff src/ tests/ + + +####################################################################################### +[testenv:cover] +deps = {[testenv]deps} + -r{toxinidir}/requirements-test.txt + coverage + nose2 +commands = + sh -c 'rm -f nosetests.xml' + coverage erase + nose2 -C --coverage src + coverage report --omit='*tests*' + coverage html -d ./cover --omit='*tests*' + coverage xml -o coverage.xml --omit=*tests* +whitelist_externals = sh + + +####################################################################################### +[testenv:flake8] +deps = flake8 + flake8-import-order +commands = + flake8 src/ tests/ + + +####################################################################################### +[testenv:pylint] +deps = {[testenv]deps} + -r{toxinidir}/requirements-test.txt + pylint==2.10.2 +commands = + pylint -E src/ tests/ + + +####################################################################################### +[testenv:safety] +setenv = + LC_ALL=C.UTF-8 + LANG=C.UTF-8 +deps = {[testenv]deps} + safety +commands = + - safety check --full-report + + +####################################################################################### +[testenv:yamllint] +deps = {[testenv]deps} + -r{toxinidir}/requirements-test.txt + yamllint +commands = yamllint . + +####################################################################################### +[testenv:build] +passenv=HTTP_PROXY HTTPS_PROXY NO_PROXY +whitelist_externals = + charmcraft + sh +commands = + charmcraft pack + sh -c 'ubuntu_version=20.04; \ + architectures="amd64-aarch64-arm64"; \ + charm_name=`cat metadata.yaml | grep -E "^name: " | cut -f 2 -d " "`; \ + mv $charm_name"_ubuntu-"$ubuntu_version-$architectures.charm $charm_name.charm' + +####################################################################################### +[flake8] +ignore = + W291, + W293, + W503, + E123, + E125, + E226, + E241, +exclude = + .git, + __pycache__, + .tox, +max-line-length = 120 +show-source = True +builtins = _ +max-complexity = 10 +import-order-style = google -- 2.17.1