Skip to content
Snippets Groups Projects
Commit abe7321e authored by sousaedu's avatar sousaedu Committed by Mark Beierl
Browse files

Refactoring LCM charm to use Operator Framework


This refactoring work includes tests.

Note 1: old charm is in lcm-k8s folder.
Note 2: relation-departed is currently not tested because there is
        no function to remove a relation in the Testing Harness.
        There is currently one issue open and the Charmcraft team
        should provide feedback soon.

Change-Id: Iae2a0e59e74fab8820d2a9a0ef3f3ec449349dbe
Signed-off-by: default avatarsousaedu <eduardo.sousa@canonical.com>
parent 4df5a461
No related branches found
No related tags found
No related merge requests found
# 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
##
venv
.vscode
build
lcm.charm
.coverage
.stestr
cover
# 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
##
---
extends: default
yaml-files:
- "*.yaml"
- "*.yml"
- ".yamllint"
ignore: |
.tox
build/
mod/
lib/
<!-- 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 -->
# LCM operator Charm for Kubernetes
## Requirements
# 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
##
options:
vca_host:
type: string
description: "The VCA host."
default: "admin"
vca_port:
type: int
description: "The VCA port."
default: 17070
vca_user:
type: string
description: "The VCA user name."
default: "admin"
vca_password:
type: string
description: "The VCA user password."
default: "secret"
vca_pubkey:
type: string
description: "The VCA public key."
default: "secret"
vca_cacert:
type: string
description: "The VCA cacert."
default: ""
vca_apiproxy:
type: string
description: "The VCA api proxy (native charms)"
default: ""
vca_cloud:
type: string
description: "The VCA lxd cloud name"
default: "localhost"
vca_k8s_cloud:
type: string
description: "The VCA K8s cloud name"
default: "k8scloud"
database_commonkey:
description: Database common key
type: string
default: osm
log_level:
description: "Log Level"
type: string
default: "INFO"
# 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
##
name: lcm
summary: OSM Lifecycle Management (LCM)
description: |
A CAAS charm to deploy OSM's Lifecycle Management (LCM).
series:
- kubernetes
tags:
- kubernetes
- osm
- lcm
min-juju-version: 2.8.0
deployment:
type: stateless
service: cluster
resources:
image:
type: oci-image
description: OSM docker image for LCM
upstream-source: "opensourcemano/lcm:8"
requires:
kafka:
interface: kafka
mongodb:
interface: mongodb
ro:
interface: osm-ro
# 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
##
ops
pydantic
git+https://github.com/juju-solutions/resource-oci-image/@c5778285d332edf3d9a538f9d0c06154b7ec1b0b#egg=oci-image
#!/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 ValidationError
from typing import Any, Dict, NoReturn
from ops.charm import CharmBase, CharmEvents
from ops.framework import EventBase, EventSource, StoredState
from ops.main import main
from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus
from oci_image import OCIImageResource, OCIImageResourceError
from pod_spec import make_pod_spec
logger = logging.getLogger(__name__)
LCM_PORT = 9999
class ConfigurePodEvent(EventBase):
"""Configure Pod event"""
pass
class LcmEvents(CharmEvents):
"""LCM Events"""
configure_pod = EventSource(ConfigurePodEvent)
class LcmCharm(CharmBase):
"""LCM Charm."""
state = StoredState()
on = LcmEvents()
def __init__(self, *args) -> NoReturn:
"""LCM Charm constructor."""
super().__init__(*args)
# Internal state initialization
self.state.set_default(pod_spec=None)
# Message bus data initialization
self.state.set_default(message_host=None)
self.state.set_default(message_port=None)
# Database data initialization
self.state.set_default(database_uri=None)
# RO data initialization
self.state.set_default(ro_host=None)
self.state.set_default(ro_port=None)
self.port = LCM_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 events
self.framework.observe(
self.on.kafka_relation_changed, self._on_kafka_relation_changed
)
self.framework.observe(
self.on.mongodb_relation_changed, self._on_mongodb_relation_changed
)
self.framework.observe(
self.on.ro_relation_changed, self._on_ro_relation_changed
)
# Registering required relation departed events
self.framework.observe(
self.on.kafka_relation_departed, self._on_kafka_relation_departed
)
self.framework.observe(
self.on.mongodb_relation_departed, self._on_mongodb_relation_departed
)
self.framework.observe(
self.on.ro_relation_departed, self._on_ro_relation_departed
)
def _on_kafka_relation_changed(self, event: EventBase) -> NoReturn:
"""Reads information about the kafka relation.
Args:
event (EventBase): Kafka relation event.
"""
data_loc = event.unit if event.unit else event.app
message_host = event.relation.data[data_loc].get("host")
message_port = event.relation.data[data_loc].get("port")
if (
message_host
and message_port
and (
self.state.message_host != message_host
or self.state.message_port != message_port
)
):
self.state.message_host = message_host
self.state.message_port = message_port
self.on.configure_pod.emit()
def _on_kafka_relation_departed(self, event: EventBase) -> NoReturn:
"""Clears data from kafka relation.
Args:
event (EventBase): Kafka relation event.
"""
self.state.message_host = None
self.state.message_port = None
self.on.configure_pod.emit()
def _on_mongodb_relation_changed(self, event: EventBase) -> NoReturn:
"""Reads information about the DB relation.
Args:
event (EventBase): DB relation event.
"""
data_loc = event.unit if event.unit else event.app
database_uri = event.relation.data[data_loc].get("connection_string")
if database_uri and self.state.database_uri != database_uri:
self.state.database_uri = database_uri
self.on.configure_pod.emit()
def _on_mongodb_relation_departed(self, event: EventBase) -> NoReturn:
"""Clears data from mongodb relation.
Args:
event (EventBase): DB relation event.
"""
self.state.database_uri = None
self.on.configure_pod.emit()
def _on_ro_relation_changed(self, event: EventBase) -> NoReturn:
"""Reads information about the RO relation.
Args:
event (EventBase): Keystone relation event.
"""
data_loc = event.unit if event.unit else event.app
ro_host = event.relation.data[data_loc].get("host")
ro_port = event.relation.data[data_loc].get("port")
if (
ro_host
and ro_port
and (self.state.ro_host != ro_host or self.state.ro_port != ro_port)
):
self.state.ro_host = ro_host
self.state.ro_port = ro_port
self.on.configure_pod.emit()
def _on_ro_relation_departed(self, event: EventBase) -> NoReturn:
"""Clears data from ro relation.
Args:
event (EventBase): Keystone relation event.
"""
self.state.ro_host = None
self.state.ro_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 = {
"kafka": self.state.message_host,
"mongodb": self.state.database_uri,
"ro": self.state.ro_host,
}
missing_relations = [k for k, v in data_status.items() if not v]
return ", ".join(missing_relations)
@property
def relation_state(self) -> Dict[str, Any]:
"""Collects relation state configuration for pod spec assembly.
Returns:
Dict[str, Any]: relation state information.
"""
relation_state = {
"message_host": self.state.message_host,
"message_port": self.state.message_port,
"database_uri": self.state.database_uri,
"ro_host": self.state.ro_host,
"ro_port": self.state.ro_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(
"Waiting for {0} relation{1}".format(
missing, "s" if "," in missing else ""
)
)
return
if not self.unit.is_leader():
self.unit.status = ActiveStatus("ready")
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,
self.relation_state,
self.model.app.name,
self.port,
)
except ValidationError 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(LcmCharm)
#!/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, constr, PositiveInt, validator
from typing import Any, Dict, List, Optional
logger = logging.getLogger(__name__)
class ConfigData(BaseModel):
"""Configuration data model."""
database_commonkey: constr(min_length=1)
log_level: constr(regex=r"^(INFO|DEBUG)$")
vca_host: constr(min_length=1)
vca_port: PositiveInt
vca_user: constr(min_length=1)
vca_pubkey: constr(min_length=1)
vca_password: constr(min_length=1)
vca_cacert: str
vca_cloud: constr(min_length=1)
vca_k8s_cloud: constr(min_length=1)
vca_apiproxy: Optional[constr(min_length=1)]
@validator("vca_apiproxy", pre=True, always=True)
def validate_vca_apiproxy(cls, value, values, **kwargs):
if not value:
return None
return value
class RelationData(BaseModel):
"""Relation data model."""
ro_host: constr(min_length=1)
ro_port: PositiveInt
message_host: constr(min_length=1)
message_port: PositiveInt
database_uri: constr(regex=r"^(mongodb://)")
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": "lcm", "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 = {
# General configuration
"ALLOW_ANONYMOUS_LOGIN": "yes",
"OSMLCM_GLOBAL_LOGLEVEL": config["log_level"],
# RO configuration
"OSMLCM_RO_HOST": relation_state["ro_host"],
"OSMLCM_RO_PORT": relation_state["ro_port"],
"OSMLCM_RO_TENANT": "osm",
# Kafka configuration
"OSMLCM_MESSAGE_DRIVER": "kafka",
"OSMLCM_MESSAGE_HOST": relation_state["message_host"],
"OSMLCM_MESSAGE_PORT": relation_state["message_port"],
# Database configuration
"OSMLCM_DATABASE_DRIVER": "mongo",
"OSMLCM_DATABASE_URI": relation_state["database_uri"],
"OSMLCM_DATABASE_COMMONKEY": config["database_commonkey"],
# Storage configuration
"OSMLCM_STORAGE_DRIVER": "mongo",
"OSMLCM_STORAGE_PATH": "/app/storage",
"OSMLCM_STORAGE_COLLECTION": "files",
"OSMLCM_STORAGE_URI": relation_state["database_uri"],
# VCA configuration
"OSMLCM_VCA_HOST": config["vca_host"],
"OSMLCM_VCA_PORT": config["vca_port"],
"OSMLCM_VCA_USER": config["vca_user"],
"OSMLCM_VCA_PUBKEY": config["vca_pubkey"],
"OSMLCM_VCA_SECRET": config["vca_password"],
"OSMLCM_VCA_CACERT": config["vca_cacert"],
"OSMLCM_VCA_CLOUD": config["vca_cloud"],
"OSMLCM_VCA_K8S_CLOUD": config["vca_k8s_cloud"],
}
if "vca_apiproxy" in config and config["vca_apiproxy"]:
envconfig["OSMLCM_VCA_APIPROXY"] = config["vca_apiproxy"]
return envconfig
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 {
"httpGet": {
"path": "/osm/",
"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 {
"httpGet": {
"path": "/osm/",
"port": port,
},
"initialDelaySeconds": 45,
"timeoutSeconds": 5,
}
def make_pod_spec(
image_info: Dict[str, str],
config: Dict[str, Any],
relation_state: Dict[str, Any],
app_name: str = "lcm",
port: int = 9999,
) -> 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 "lcm".
port (int, optional): Port for the container. Defaults to 9999.
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(port)
env_config = _make_pod_envconfig(config, relation_state)
return {
"version": 3,
"containers": [
{
"name": app_name,
"imageDetails": image_info,
"imagePullPolicy": "Always",
"ports": ports,
"envConfig": env_config,
}
],
"kubernetesResources": {
"ingressResources": [],
},
}
#!/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
#!/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
from ops.model import BlockedStatus
from ops.testing import Harness
from charm import LcmCharm
class TestCharm(unittest.TestCase):
"""LCM Charm unit tests."""
def setUp(self) -> NoReturn:
"""Test setup"""
self.harness = Harness(LcmCharm)
self.harness.set_leader(is_leader=True)
self.harness.begin()
def test_on_start_without_relations(self) -> NoReturn:
"""Test installation without any relation."""
self.harness.charm.on.start.emit()
# Verifying status
self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
# Verifying status message
self.assertGreater(len(self.harness.charm.unit.status.message), 0)
self.assertTrue(
self.harness.charm.unit.status.message.startswith("Waiting for ")
)
self.assertIn("kafka", self.harness.charm.unit.status.message)
self.assertIn("mongodb", self.harness.charm.unit.status.message)
self.assertIn("ro", self.harness.charm.unit.status.message)
self.assertTrue(self.harness.charm.unit.status.message.endswith(" relations"))
def test_on_start_with_relations(self) -> NoReturn:
"""Test deployment without keystone."""
expected_result = {
"version": 3,
"containers": [
{
"name": "lcm",
"imageDetails": self.harness.charm.image.fetch(),
"imagePullPolicy": "Always",
"ports": [
{
"name": "lcm",
"containerPort": 9999,
"protocol": "TCP",
}
],
"envConfig": {
"ALLOW_ANONYMOUS_LOGIN": "yes",
"OSMLCM_GLOBAL_LOGLEVEL": "INFO",
"OSMLCM_RO_HOST": "ro",
"OSMLCM_RO_PORT": 9090,
"OSMLCM_RO_TENANT": "osm",
"OSMLCM_MESSAGE_DRIVER": "kafka",
"OSMLCM_MESSAGE_HOST": "kafka",
"OSMLCM_MESSAGE_PORT": 9092,
"OSMLCM_DATABASE_DRIVER": "mongo",
"OSMLCM_DATABASE_URI": "mongodb://mongo:27017",
"OSMLCM_DATABASE_COMMONKEY": "osm",
"OSMLCM_STORAGE_DRIVER": "mongo",
"OSMLCM_STORAGE_PATH": "/app/storage",
"OSMLCM_STORAGE_COLLECTION": "files",
"OSMLCM_STORAGE_URI": "mongodb://mongo:27017",
"OSMLCM_VCA_HOST": "admin",
"OSMLCM_VCA_PORT": 17070,
"OSMLCM_VCA_USER": "admin",
"OSMLCM_VCA_PUBKEY": "secret",
"OSMLCM_VCA_SECRET": "secret",
"OSMLCM_VCA_CACERT": "",
"OSMLCM_VCA_CLOUD": "localhost",
"OSMLCM_VCA_K8S_CLOUD": "k8scloud",
},
}
],
"kubernetesResources": {"ingressResources": []},
}
self.harness.charm.on.start.emit()
# Check if kafka datastore is initialized
self.assertIsNone(self.harness.charm.state.message_host)
self.assertIsNone(self.harness.charm.state.message_port)
# Check if mongodb datastore is initialized
self.assertIsNone(self.harness.charm.state.database_uri)
# Check if RO datastore is initialized
self.assertIsNone(self.harness.charm.state.ro_host)
self.assertIsNone(self.harness.charm.state.ro_port)
# Initializing the kafka relation
kafka_relation_id = self.harness.add_relation("kafka", "kafka")
self.harness.add_relation_unit(kafka_relation_id, "kafka/0")
self.harness.update_relation_data(
kafka_relation_id, "kafka/0", {"host": "kafka", "port": 9092}
)
# Initializing the mongo relation
mongodb_relation_id = self.harness.add_relation("mongodb", "mongodb")
self.harness.add_relation_unit(mongodb_relation_id, "mongodb/0")
self.harness.update_relation_data(
mongodb_relation_id,
"mongodb/0",
{"connection_string": "mongodb://mongo:27017"},
)
# Initializing the RO relation
ro_relation_id = self.harness.add_relation("ro", "ro")
self.harness.add_relation_unit(ro_relation_id, "ro/0")
self.harness.update_relation_data(
ro_relation_id, "ro", {"host": "ro", "port": 9090}
)
# Checking if kafka data is stored
self.assertEqual(self.harness.charm.state.message_host, "kafka")
self.assertEqual(self.harness.charm.state.message_port, 9092)
# Checking if mongodb data is stored
self.assertEqual(self.harness.charm.state.database_uri, "mongodb://mongo:27017")
# Checking if RO data is stored
self.assertEqual(self.harness.charm.state.ro_host, "ro")
self.assertEqual(self.harness.charm.state.ro_port, 9090)
# Verifying status
self.assertNotIsInstance(self.harness.charm.unit.status, BlockedStatus)
pod_spec, _ = self.harness.get_pod_spec()
self.assertDictEqual(expected_result, pod_spec)
def test_on_kafka_relation_app_changed(self) -> NoReturn:
"""Test to see if kafka relation is updated."""
self.harness.charm.on.start.emit()
self.assertIsNone(self.harness.charm.state.message_host)
self.assertIsNone(self.harness.charm.state.message_port)
relation_id = self.harness.add_relation("kafka", "kafka")
self.harness.add_relation_unit(relation_id, "kafka/0")
self.harness.update_relation_data(
relation_id, "kafka", {"host": "kafka", "port": 9092}
)
self.assertEqual(self.harness.charm.state.message_host, "kafka")
self.assertEqual(self.harness.charm.state.message_port, 9092)
# Verifying status
self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
# Verifying status message
self.assertGreater(len(self.harness.charm.unit.status.message), 0)
self.assertTrue(
self.harness.charm.unit.status.message.startswith("Waiting for ")
)
self.assertNotIn("kafka", self.harness.charm.unit.status.message)
self.assertIn("mongodb", self.harness.charm.unit.status.message)
self.assertIn("ro", self.harness.charm.unit.status.message)
self.assertTrue(self.harness.charm.unit.status.message.endswith(" relations"))
def test_on_kafka_relation_unit_changed(self) -> NoReturn:
"""Test to see if kafka relation is updated."""
self.harness.charm.on.start.emit()
self.assertIsNone(self.harness.charm.state.message_host)
self.assertIsNone(self.harness.charm.state.message_port)
relation_id = self.harness.add_relation("kafka", "kafka")
self.harness.add_relation_unit(relation_id, "kafka/0")
self.harness.update_relation_data(
relation_id, "kafka/0", {"host": "kafka", "port": 9092}
)
self.assertEqual(self.harness.charm.state.message_host, "kafka")
self.assertEqual(self.harness.charm.state.message_port, 9092)
# Verifying status
self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
# Verifying status message
self.assertGreater(len(self.harness.charm.unit.status.message), 0)
self.assertTrue(
self.harness.charm.unit.status.message.startswith("Waiting for ")
)
self.assertNotIn("kafka", self.harness.charm.unit.status.message)
self.assertIn("mongodb", self.harness.charm.unit.status.message)
self.assertIn("ro", self.harness.charm.unit.status.message)
self.assertTrue(self.harness.charm.unit.status.message.endswith(" relations"))
def test_on_mongodb_app_relation_changed(self) -> NoReturn:
"""Test to see if mongodb relation is updated."""
self.harness.charm.on.start.emit()
self.assertIsNone(self.harness.charm.state.database_uri)
relation_id = self.harness.add_relation("mongodb", "mongodb")
self.harness.add_relation_unit(relation_id, "mongodb/0")
self.harness.update_relation_data(
relation_id, "mongodb", {"connection_string": "mongodb://mongo:27017"}
)
self.assertEqual(self.harness.charm.state.database_uri, "mongodb://mongo:27017")
# Verifying status
self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
# Verifying status message
self.assertGreater(len(self.harness.charm.unit.status.message), 0)
self.assertTrue(
self.harness.charm.unit.status.message.startswith("Waiting for ")
)
self.assertIn("kafka", self.harness.charm.unit.status.message)
self.assertNotIn("mongodb", self.harness.charm.unit.status.message)
self.assertIn("ro", self.harness.charm.unit.status.message)
self.assertTrue(self.harness.charm.unit.status.message.endswith(" relations"))
def test_on_mongodb_unit_relation_changed(self) -> NoReturn:
"""Test to see if mongodb relation is updated."""
self.harness.charm.on.start.emit()
self.assertIsNone(self.harness.charm.state.database_uri)
relation_id = self.harness.add_relation("mongodb", "mongodb")
self.harness.add_relation_unit(relation_id, "mongodb/0")
self.harness.update_relation_data(
relation_id, "mongodb/0", {"connection_string": "mongodb://mongo:27017"}
)
self.assertEqual(self.harness.charm.state.database_uri, "mongodb://mongo:27017")
# Verifying status
self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
# Verifying status message
self.assertGreater(len(self.harness.charm.unit.status.message), 0)
self.assertTrue(
self.harness.charm.unit.status.message.startswith("Waiting for ")
)
self.assertIn("kafka", self.harness.charm.unit.status.message)
self.assertNotIn("mongodb", self.harness.charm.unit.status.message)
self.assertIn("ro", self.harness.charm.unit.status.message)
self.assertTrue(self.harness.charm.unit.status.message.endswith(" relations"))
def test_on_ro_app_relation_changed(self) -> NoReturn:
"""Test to see if RO relation is updated."""
self.harness.charm.on.start.emit()
self.assertIsNone(self.harness.charm.state.ro_host)
self.assertIsNone(self.harness.charm.state.ro_port)
relation_id = self.harness.add_relation("ro", "ro")
self.harness.add_relation_unit(relation_id, "ro/0")
self.harness.update_relation_data(
relation_id, "ro", {"host": "ro", "port": 9090}
)
self.assertEqual(self.harness.charm.state.ro_host, "ro")
self.assertEqual(self.harness.charm.state.ro_port, 9090)
# Verifying status
self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
# Verifying status message
self.assertGreater(len(self.harness.charm.unit.status.message), 0)
self.assertTrue(
self.harness.charm.unit.status.message.startswith("Waiting for ")
)
self.assertIn("kafka", self.harness.charm.unit.status.message)
self.assertIn("mongodb", self.harness.charm.unit.status.message)
self.assertNotIn("ro", self.harness.charm.unit.status.message)
self.assertTrue(self.harness.charm.unit.status.message.endswith(" relations"))
def test_on_ro_unit_relation_changed(self) -> NoReturn:
"""Test to see if RO relation is updated."""
self.harness.charm.on.start.emit()
self.assertIsNone(self.harness.charm.state.ro_host)
self.assertIsNone(self.harness.charm.state.ro_port)
relation_id = self.harness.add_relation("ro", "ro")
self.harness.add_relation_unit(relation_id, "ro/0")
self.harness.update_relation_data(
relation_id, "ro/0", {"host": "ro", "port": 9090}
)
self.assertEqual(self.harness.charm.state.ro_host, "ro")
self.assertEqual(self.harness.charm.state.ro_port, 9090)
# Verifying status
self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
# Verifying status message
self.assertGreater(len(self.harness.charm.unit.status.message), 0)
self.assertTrue(
self.harness.charm.unit.status.message.startswith("Waiting for ")
)
self.assertIn("kafka", self.harness.charm.unit.status.message)
self.assertIn("mongodb", self.harness.charm.unit.status.message)
self.assertNotIn("ro", self.harness.charm.unit.status.message)
self.assertTrue(self.harness.charm.unit.status.message.endswith(" relations"))
if __name__ == "__main__":
unittest.main()
#!/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 pydantic import ValidationError
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 = 9999
expected_result = [
{
"name": "lcm",
"containerPort": port,
"protocol": "TCP",
}
]
pod_ports = pod_spec._make_pod_ports(9999)
self.assertListEqual(expected_result, pod_ports)
def test_make_pod_envconfig_without_vca_apiproxy(self) -> NoReturn:
"""Teting make pod envconfig without vca_apiproxy configuration."""
config = {
"database_commonkey": "commonkey",
"log_level": "INFO",
"vca_host": "vca",
"vca_port": 1212,
"vca_user": "vca_user",
"vca_pubkey": "vca_pubkey",
"vca_password": "vca_password",
"vca_cacert": "vca_cacert",
"vca_cloud": "vca_cloud",
"vca_k8s_cloud": "vca_k8s_cloud",
}
relation_state = {
"message_host": "kafka",
"message_port": 2181,
"database_uri": "mongodb://mongo",
"ro_host": "ro",
"ro_port": 9090,
}
expected_result = {
"ALLOW_ANONYMOUS_LOGIN": "yes",
"OSMLCM_GLOBAL_LOGLEVEL": config["log_level"],
"OSMLCM_RO_HOST": relation_state["ro_host"],
"OSMLCM_RO_PORT": relation_state["ro_port"],
"OSMLCM_RO_TENANT": "osm",
"OSMLCM_MESSAGE_DRIVER": "kafka",
"OSMLCM_MESSAGE_HOST": relation_state["message_host"],
"OSMLCM_MESSAGE_PORT": relation_state["message_port"],
"OSMLCM_DATABASE_DRIVER": "mongo",
"OSMLCM_DATABASE_URI": relation_state["database_uri"],
"OSMLCM_DATABASE_COMMONKEY": config["database_commonkey"],
"OSMLCM_STORAGE_DRIVER": "mongo",
"OSMLCM_STORAGE_PATH": "/app/storage",
"OSMLCM_STORAGE_COLLECTION": "files",
"OSMLCM_STORAGE_URI": relation_state["database_uri"],
"OSMLCM_VCA_HOST": config["vca_host"],
"OSMLCM_VCA_PORT": config["vca_port"],
"OSMLCM_VCA_USER": config["vca_user"],
"OSMLCM_VCA_PUBKEY": config["vca_pubkey"],
"OSMLCM_VCA_SECRET": config["vca_password"],
"OSMLCM_VCA_CACERT": config["vca_cacert"],
"OSMLCM_VCA_CLOUD": config["vca_cloud"],
"OSMLCM_VCA_K8S_CLOUD": config["vca_k8s_cloud"],
}
pod_envconfig = pod_spec._make_pod_envconfig(config, relation_state)
self.assertDictEqual(expected_result, pod_envconfig)
def test_make_pod_envconfig_with_vca_apiproxy(self) -> NoReturn:
"""Teting make pod envconfig with vca_apiproxy configuration."""
config = {
"database_commonkey": "commonkey",
"log_level": "INFO",
"vca_host": "vca",
"vca_port": 1212,
"vca_user": "vca_user",
"vca_pubkey": "vca_pubkey",
"vca_password": "vca_password",
"vca_cacert": "vca_cacert",
"vca_cloud": "vca_cloud",
"vca_k8s_cloud": "vca_k8s_cloud",
"vca_apiproxy": "vca_apiproxy",
}
relation_state = {
"message_host": "kafka",
"message_port": 2181,
"database_uri": "mongodb://mongo",
"ro_host": "ro",
"ro_port": 9090,
}
expected_result = {
"ALLOW_ANONYMOUS_LOGIN": "yes",
"OSMLCM_GLOBAL_LOGLEVEL": config["log_level"],
"OSMLCM_RO_HOST": relation_state["ro_host"],
"OSMLCM_RO_PORT": relation_state["ro_port"],
"OSMLCM_RO_TENANT": "osm",
"OSMLCM_MESSAGE_DRIVER": "kafka",
"OSMLCM_MESSAGE_HOST": relation_state["message_host"],
"OSMLCM_MESSAGE_PORT": relation_state["message_port"],
"OSMLCM_DATABASE_DRIVER": "mongo",
"OSMLCM_DATABASE_URI": relation_state["database_uri"],
"OSMLCM_DATABASE_COMMONKEY": config["database_commonkey"],
"OSMLCM_STORAGE_DRIVER": "mongo",
"OSMLCM_STORAGE_PATH": "/app/storage",
"OSMLCM_STORAGE_COLLECTION": "files",
"OSMLCM_STORAGE_URI": relation_state["database_uri"],
"OSMLCM_VCA_HOST": config["vca_host"],
"OSMLCM_VCA_PORT": config["vca_port"],
"OSMLCM_VCA_USER": config["vca_user"],
"OSMLCM_VCA_PUBKEY": config["vca_pubkey"],
"OSMLCM_VCA_SECRET": config["vca_password"],
"OSMLCM_VCA_CACERT": config["vca_cacert"],
"OSMLCM_VCA_CLOUD": config["vca_cloud"],
"OSMLCM_VCA_K8S_CLOUD": config["vca_k8s_cloud"],
"OSMLCM_VCA_APIPROXY": config["vca_apiproxy"],
}
pod_envconfig = pod_spec._make_pod_envconfig(config, relation_state)
self.assertDictEqual(expected_result, pod_envconfig)
def test_make_startup_probe(self) -> NoReturn:
"""Testing make startup probe."""
expected_result = {
"exec": {"command": ["/usr/bin/pgrep python3"]},
"initialDelaySeconds": 60,
"timeoutSeconds": 5,
}
startup_probe = pod_spec._make_startup_probe()
self.assertDictEqual(expected_result, startup_probe)
def test_make_readiness_probe(self) -> NoReturn:
"""Testing make readiness probe."""
port = 9999
expected_result = {
"httpGet": {
"path": "/osm/",
"port": port,
},
"initialDelaySeconds": 45,
"timeoutSeconds": 5,
}
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 = 9999
expected_result = {
"httpGet": {
"path": "/osm/",
"port": port,
},
"initialDelaySeconds": 45,
"timeoutSeconds": 5,
}
liveness_probe = pod_spec._make_liveness_probe(port)
self.assertDictEqual(expected_result, liveness_probe)
def test_make_pod_spec(self) -> NoReturn:
"""Testing make pod spec."""
image_info = {"upstream-source": "opensourcemano/lcm:8"}
config = {
"database_commonkey": "commonkey",
"log_level": "INFO",
"vca_host": "vca",
"vca_port": 1212,
"vca_user": "vca_user",
"vca_pubkey": "vca_pubkey",
"vca_password": "vca_password",
"vca_cacert": "vca_cacert",
"vca_cloud": "vca_cloud",
"vca_k8s_cloud": "vca_k8s_cloud",
"vca_apiproxy": "vca_apiproxy",
}
relation_state = {
"message_host": "kafka",
"message_port": 2181,
"database_uri": "mongodb://mongo",
"ro_host": "ro",
"ro_port": 9090,
}
app_name = "lcm"
port = 9999
expected_result = {
"version": 3,
"containers": [
{
"name": app_name,
"imageDetails": image_info,
"imagePullPolicy": "Always",
"ports": [
{
"name": app_name,
"containerPort": port,
"protocol": "TCP",
}
],
"envConfig": {
"ALLOW_ANONYMOUS_LOGIN": "yes",
"OSMLCM_GLOBAL_LOGLEVEL": config["log_level"],
"OSMLCM_RO_HOST": relation_state["ro_host"],
"OSMLCM_RO_PORT": relation_state["ro_port"],
"OSMLCM_RO_TENANT": "osm",
"OSMLCM_MESSAGE_DRIVER": "kafka",
"OSMLCM_MESSAGE_HOST": relation_state["message_host"],
"OSMLCM_MESSAGE_PORT": relation_state["message_port"],
"OSMLCM_DATABASE_DRIVER": "mongo",
"OSMLCM_DATABASE_URI": relation_state["database_uri"],
"OSMLCM_DATABASE_COMMONKEY": config["database_commonkey"],
"OSMLCM_STORAGE_DRIVER": "mongo",
"OSMLCM_STORAGE_PATH": "/app/storage",
"OSMLCM_STORAGE_COLLECTION": "files",
"OSMLCM_STORAGE_URI": relation_state["database_uri"],
"OSMLCM_VCA_HOST": config["vca_host"],
"OSMLCM_VCA_PORT": config["vca_port"],
"OSMLCM_VCA_USER": config["vca_user"],
"OSMLCM_VCA_PUBKEY": config["vca_pubkey"],
"OSMLCM_VCA_SECRET": config["vca_password"],
"OSMLCM_VCA_CACERT": config["vca_cacert"],
"OSMLCM_VCA_CLOUD": config["vca_cloud"],
"OSMLCM_VCA_K8S_CLOUD": config["vca_k8s_cloud"],
"OSMLCM_VCA_APIPROXY": config["vca_apiproxy"],
},
}
],
"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_vca_apiproxy(self) -> NoReturn:
"""Testing make pod spec with vca_apiproxy."""
image_info = {"upstream-source": "opensourcemano/lcm:8"}
config = {
"database_commonkey": "commonkey",
"log_level": "INFO",
"vca_host": "vca",
"vca_port": 1212,
"vca_user": "vca_user",
"vca_pubkey": "vca_pubkey",
"vca_password": "vca_password",
"vca_cacert": "vca_cacert",
"vca_cloud": "vca_cloud",
"vca_k8s_cloud": "vca_k8s_cloud",
}
relation_state = {
"message_host": "kafka",
"message_port": 2181,
"database_uri": "mongodb://mongo",
"ro_host": "ro",
"ro_port": 9090,
}
app_name = "lcm"
port = 9999
expected_result = {
"version": 3,
"containers": [
{
"name": app_name,
"imageDetails": image_info,
"imagePullPolicy": "Always",
"ports": [
{
"name": app_name,
"containerPort": port,
"protocol": "TCP",
}
],
"envConfig": {
"ALLOW_ANONYMOUS_LOGIN": "yes",
"OSMLCM_GLOBAL_LOGLEVEL": config["log_level"],
"OSMLCM_RO_HOST": relation_state["ro_host"],
"OSMLCM_RO_PORT": relation_state["ro_port"],
"OSMLCM_RO_TENANT": "osm",
"OSMLCM_MESSAGE_DRIVER": "kafka",
"OSMLCM_MESSAGE_HOST": relation_state["message_host"],
"OSMLCM_MESSAGE_PORT": relation_state["message_port"],
"OSMLCM_DATABASE_DRIVER": "mongo",
"OSMLCM_DATABASE_URI": relation_state["database_uri"],
"OSMLCM_DATABASE_COMMONKEY": config["database_commonkey"],
"OSMLCM_STORAGE_DRIVER": "mongo",
"OSMLCM_STORAGE_PATH": "/app/storage",
"OSMLCM_STORAGE_COLLECTION": "files",
"OSMLCM_STORAGE_URI": relation_state["database_uri"],
"OSMLCM_VCA_HOST": config["vca_host"],
"OSMLCM_VCA_PORT": config["vca_port"],
"OSMLCM_VCA_USER": config["vca_user"],
"OSMLCM_VCA_PUBKEY": config["vca_pubkey"],
"OSMLCM_VCA_SECRET": config["vca_password"],
"OSMLCM_VCA_CACERT": config["vca_cacert"],
"OSMLCM_VCA_CLOUD": config["vca_cloud"],
"OSMLCM_VCA_K8S_CLOUD": config["vca_k8s_cloud"],
},
}
],
"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_without_image_info(self) -> NoReturn:
"""Testing make pod spec without image_info."""
image_info = None
config = {
"database_commonkey": "commonkey",
"log_level": "INFO",
"vca_host": "vca",
"vca_port": 1212,
"vca_user": "vca_user",
"vca_pubkey": "vca_pubkey",
"vca_password": "vca_password",
"vca_cacert": "vca_cacert",
"vca_cloud": "vca_cloud",
"vca_k8s_cloud": "vca_k8s_cloud",
"vca_apiproxy": "vca_apiproxy",
}
relation_state = {
"message_host": "kafka",
"message_port": 2181,
"database_uri": "mongodb://mongo",
"ro_host": "ro",
"ro_port": 9090,
}
app_name = "lcm"
port = 9999
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": "opensourcemano/lcm:8"}
config = {}
relation_state = {
"message_host": "kafka",
"message_port": 2181,
"database_uri": "mongodb://mongo",
"ro_host": "ro",
"ro_port": 9090,
}
app_name = "lcm"
port = 9999
with self.assertRaises(ValidationError):
pod_spec.make_pod_spec(image_info, config, relation_state, app_name, port)
def test_make_pod_spec_without_relation_state(self) -> NoReturn:
"""Testing make pod spec without relation_state."""
image_info = {"upstream-source": "opensourcemano/lcm:8"}
config = {
"database_commonkey": "commonkey",
"log_level": "INFO",
"vca_host": "vca",
"vca_port": 1212,
"vca_user": "vca_user",
"vca_pubkey": "vca_pubkey",
"vca_password": "vca_password",
"vca_cacert": "vca_cacert",
"vca_cloud": "vca_cloud",
"vca_k8s_cloud": "vca_k8s_cloud",
"vca_apiproxy": "vca_apiproxy",
}
relation_state = {}
app_name = "lcm"
port = 9999
with self.assertRaises(ValidationError):
pod_spec.make_pod_spec(image_info, config, relation_state, app_name, port)
if __name__ == "__main__":
unittest.main()
# 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
##
[tox]
skipsdist = True
envlist = unit, lint
sitepackages = False
skip_missing_interpreters = False
[testenv]
basepython = python3
setenv =
PYTHONHASHSEED=0
PYTHONPATH = {toxinidir}/src
CHARM_NAME = lcm
[testenv:build]
passenv=HTTP_PROXY HTTPS_PROXY NO_PROXY
whitelist_externals =
charmcraft
rm
unzip
commands =
rm -rf release lcm.charm
charmcraft build
unzip lcm.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
pydantic
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