Skip to content
Snippets Groups Projects
Commit 2459af69 authored by sousaedu's avatar sousaedu
Browse files

Refactoring Prometheus Charm to use Operator Framework


Change-Id: Ib1b33bd5e6b91d2689a7c3abdfd834de20ceed88
Signed-off-by: default avatarsousaedu <eduardo.sousa@canonical.com>
parent 2f0afc11
No related branches found
No related tags found
No related merge requests found
Showing with 1998 additions and 0 deletions
# 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
prometheus.charm
.coverage
.stestr
cover
# 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
build/
mod/
lib/
<!-- 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 -->
# Prometheus operator Charm for Kubernetes
## Requirements
# 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_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: ""
# 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: 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:
image:
type: oci-image
description: OSM docker image for Prometheus
upstream-source: "ubuntu/prometheus:latest"
provides:
prometheus:
interface: prometheus
# 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
##
ops
git+https://github.com/juju-solutions/resource-oci-image/@c5778285d332edf3d9a538f9d0c06154b7ec1b0b#egg=oci-image
#!/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
##
import logging
from typing import Dict, List, NoReturn
from ops.charm import CharmBase
from ops.framework import EventBase, StoredState
from ops.main import main
from ops.model import ActiveStatus, Application, BlockedStatus, MaintenanceStatus, Unit
from oci_image import OCIImageResource, OCIImageResourceError
from pod_spec import make_pod_spec
logger = logging.getLogger(__name__)
PROMETHEUS_PORT = 9090
class RelationsMissing(Exception):
def __init__(self, missing_relations: List):
self.message = ""
if missing_relations and isinstance(missing_relations, list):
self.message += f'Waiting for {", ".join(missing_relations)} relation'
if "," in self.message:
self.message += "s"
class RelationDefinition:
def __init__(self, relation_name: str, keys: List, source_type):
if source_type != Application and source_type != Unit:
raise TypeError(
"source_type should be ops.model.Application or ops.model.Unit"
)
self.relation_name = relation_name
self.keys = keys
self.source_type = source_type
def check_missing_relation_data(
data: Dict,
expected_relations_data: List[RelationDefinition],
):
missing_relations = []
for relation_data in expected_relations_data:
if not all(
f"{relation_data.relation_name}_{k}" in data for k in relation_data.keys
):
missing_relations.append(relation_data.relation_name)
if missing_relations:
raise RelationsMissing(missing_relations)
def get_relation_data(
charm: CharmBase,
relation_data: RelationDefinition,
) -> Dict:
data = {}
relation = charm.model.get_relation(relation_data.relation_name)
if relation:
self_app_unit = (
charm.app if relation_data.source_type == Application else charm.unit
)
expected_type = relation_data.source_type
for app_unit in relation.data:
if app_unit != self_app_unit and isinstance(app_unit, expected_type):
if all(k in relation.data[app_unit] for k in relation_data.keys):
for k in relation_data.keys:
data[f"{relation_data.relation_name}_{k}"] = relation.data[
app_unit
].get(k)
break
return data
class PrometheusCharm(CharmBase):
"""Prometheus Charm."""
state = StoredState()
def __init__(self, *args) -> NoReturn:
"""Prometheus Charm constructor."""
super().__init__(*args)
# Internal state initialization
self.state.set_default(pod_spec=None)
self.port = PROMETHEUS_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)
# Registering provided relation events
self.framework.observe(
self.on.prometheus_relation_joined, self._publish_prometheus_info
)
def _publish_prometheus_info(self, event: EventBase) -> NoReturn:
"""Publishes Prometheus information.
Args:
event (EventBase): Prometheus relation event.
"""
if self.unit.is_leader():
rel_data = {
"host": self.model.app.name,
"port": str(PROMETHEUS_PORT),
}
for k, v in rel_data.items():
event.relation.data[self.app][k] = v
@property
def relations_requirements(self):
return []
def get_relation_state(self):
relation_state = {}
for relation_requirements in self.relations_requirements:
data = get_relation_data(self, relation_requirements)
relation_state = {**relation_state, **data}
check_missing_relation_data(relation_state, self.relations_requirements)
return relation_state
def configure_pod(self, _=None) -> NoReturn:
"""Assemble the pod spec and apply it, if possible.
Args:
event (EventBase): Hook or Relation event that started the
function.
"""
if not self.unit.is_leader():
self.unit.status = ActiveStatus("ready")
return
relation_state = None
try:
relation_state = self.get_relation_state()
except RelationsMissing as exc:
logger.exception("Relation missing error")
self.unit.status = BlockedStatus(exc.message)
return
self.unit.status = MaintenanceStatus("Assembling pod 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
try:
pod_spec = make_pod_spec(
image_info,
self.model.config,
relation_state,
self.model.app.name,
self.port,
)
except ValueError as exc:
logger.exception("Config/Relation data validation error")
self.unit.status = BlockedStatus(str(exc))
return
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(PrometheusCharm)
#!/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,
}
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:"
" scrape_interval: 15s"
" evaluation_interval: 15s"
"alerting:"
" alertmanagers:"
" - static_configs:"
" - targets:"
"rule_files:"
"scrape_configs:"
" - job_name: 'prometheus'"
" static_configs:"
" - targets: [{}]".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.
"""
return [
"sh",
"-c",
"/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")
),
]
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 [],
},
}
#!/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
sys.path.append("src")
oci_image = mock.MagicMock()
sys.modules["oci_image"] = oci_image
This diff is collapsed.
This diff is collapsed.
# 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]
skipsdist = True
envlist = unit, lint
sitepackages = False
skip_missing_interpreters = False
[testenv]
basepython = python3
setenv =
PYTHONHASHSEED=0
PYTHONPATH = {toxinidir}/src
CHARM_NAME = prometheus
[testenv:build]
passenv=HTTP_PROXY HTTPS_PROXY NO_PROXY
whitelist_externals =
charmcraft
rm
unzip
commands =
rm -rf release prometheus.charm
charmcraft build
unzip prometheus.charm -d release
[testenv:unit]
commands =
coverage erase
stestr run --slowest --test-path=./tests --top-dir=./
coverage combine
coverage html -d cover
coverage xml -o cover/coverage.xml
coverage report
deps =
coverage
stestr
mock
ops
setenv =
{[testenv]setenv}
PYTHON=coverage run
[testenv:lint]
deps =
black
yamllint
flake8
commands =
black --check --diff . --exclude "build/|.tox/|mod/|lib/"
yamllint .
flake8 . --max-line-length=100 --ignore="E501,W503,W504,F722" --exclude "build/ .tox/ mod/ lib/"
[coverage:run]
branch = True
concurrency = multiprocessing
parallel = True
source =
.
omit =
.tox/*
tests/*
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment