Skip to content

Commit

Permalink
new relation to pass gateway info (#83)
Browse files Browse the repository at this point in the history
* new relation to pass gateway info

* set model name in unit test

* fix lint

* remove debug code

* remove list comprehension for getting relations

* charm library for relation

* add lib to pythonpath
- clean up libraries
- update test

* use resource handler to create crd
or else lightkube would complain about same crd with diff signature
when using create_global_resource

* update receiver file

* fix tests

* PR review updates:
- merge charm lib files
- rename interface

* update comment

* use separate light kube client

* put mocker in fixture

* add doc

* pin dep

* fix istio-gateway not building in ci

* mock validate function instead of lightkube client

* remove relation len check

* mock lightkube client

* WIP take provider out of lib

* rename method
- only check for leadership at init
- rename gatewayprovider method
- update tests

* fix lint

* add info log if gateway relation data not sent
  • Loading branch information
agathanatasha authored May 30, 2022
1 parent d346f6c commit 78eb1af
Show file tree
Hide file tree
Showing 9 changed files with 225 additions and 13 deletions.
8 changes: 4 additions & 4 deletions charms/istio-gateway/charmcraft.yaml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
type: charm
bases:
- build-on:
- name: "ubuntu"
channel: "20.04"
- name: 'ubuntu'
channel: '20.04'
run-on:
- name: "ubuntu"
channel: "20.04"
- name: 'ubuntu'
channel: '20.04'
parts:
charm:
# Remove when pypa/setuptools_scm#713 gets fixed
Expand Down
121 changes: 121 additions & 0 deletions charms/istio-pilot/lib/charms/istio_pilot/v0/istio_gateway_name.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
"""Library for sharing istio gateway information
This library wraps the relation endpoints using the `istio-gateway-name`
interface. It provides a Python API for both requesting and providing
gateway information.
## Getting Started
### Fetch library with charmcraft
You can fetch the library using the following commands with charmcraft.
```shell
cd some-charm
charmcraft fetch-lib charms.istio_pilot.v0.istio_gateway_name
```
### Add relation to metadata.yaml
```yaml
requires:
gateway:
interface: istio_gateway_name
limit: 1
```
### Initialise the library in charm.py
```python
from charms.istio_pilot.v0.istio_gateway_name import GatewayProvider, GatewayRelationError
Class SomeCharm(CharmBase):
def __init__(self, *args):
self.gateway = GatewayProvider(self)
self.framework.observe(self.on.some_event_emitted, self.some_event_function)
def some_event_function():
# use the getter function wherever the info is needed
try:
gateway_data = self.gateway_relation.get_relation_data()
except GatewayRelationError as error:
...
```
"""

import logging
from ops.framework import Object
from ops.model import Application

# Increment this major API version when introducing breaking changes
LIBAPI = 0

# Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version
LIBPATCH = 1


DEFAULT_RELATION_NAME = "gateway"
DEFAULT_INTERFACE_NAME = "istio-gateway-name"

logger = logging.getLogger(__name__)


class GatewayRelationError(Exception):
pass


class GatewayRelationMissingError(GatewayRelationError):
def __init__(self):
self.message = "Missing gateway relation with istio-pilot"
super().__init__(self.message)


class GatewayRelationTooManyError(GatewayRelationError):
def __init__(self):
self.message = "Too many istio-gateway-name relations"
super().__init__(self.message)


class GatewayRelationDataMissingError(GatewayRelationError):
def __init__(self, message):
self.message = message
super().__init__(self.message)


class GatewayRequirer(Object):
def __init__(self, charm, relation_name: str = DEFAULT_RELATION_NAME):
super().__init__(charm, relation_name)
self.charm = charm
self.relation_name = relation_name

def get_relation_data(self):
if not self.model.unit.is_leader():
return
gateway = self.model.relations[self.relation_name]
if len(gateway) == 0:
raise GatewayRelationMissingError()
if len(gateway) > 1:
raise GatewayRelationTooManyError()

remote_app = [
app
for app in gateway[0].data.keys()
if isinstance(app, Application) and not app._is_our_app
][0]

data = gateway[0].data[remote_app]

if not "gateway_name" in data:
logger.error(
"Missing gateway name in gateway relation data. Waiting for gateway creation in istio-pilot"
)
raise GatewayRelationDataMissingError(
"Missing gateway name in gateway relation data. Waiting for gateway creation in istio-pilot"
)

if not "gateway_namespace" in data:
logger.error("Missing gateway namespace in gateway relation data")
raise GatewayRelationDataMissingError(
"Missing gateway namespace in gateway relation data"
)

return {
"gateway_name": data["gateway_name"],
"gateway_namespace": data["gateway_namespace"],
}
4 changes: 4 additions & 0 deletions charms/istio-pilot/metadata.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,9 @@ provides:
interface: ingress-auth
schema: https://raw.githubusercontent.com/canonical/operator-schemas/master/ingress-auth.yaml
versions: [v1]
gateway:
interface: istio-gateway-name
description: |
Provides gateway name related Juju application
assumes:
- juju >= 2.9.0
1 change: 1 addition & 0 deletions charms/istio-pilot/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ jinja2<3.1
ops<1.4.0
requests<2.27.0
serialized-data-interface<0.4
oci-image

# Temporarily using ca-scribner's branch that adds implicit custom
# resource creation. Change this after those changes get merged into master
Expand Down
23 changes: 23 additions & 0 deletions charms/istio-pilot/src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from ops.model import ActiveStatus, BlockedStatus, WaitingStatus
from serialized_data_interface import NoCompatibleVersions, NoVersionsListed, get_interfaces
from resources_handler import ResourceHandler
from istio_gateway_name_provider import GatewayProvider, DEFAULT_RELATION_NAME


class Operator(CharmBase):
Expand Down Expand Up @@ -45,12 +46,16 @@ def __init__(self, *args):
"auth_filter.yaml.j2",
"virtual_service.yaml.j2",
]
self.gateway = GatewayProvider(self)

self.framework.observe(self.on.install, self.install)
self.framework.observe(self.on.remove, self.remove)

self.framework.observe(self.on.config_changed, self.handle_default_gateway)

self.framework.observe(
self.on[DEFAULT_RELATION_NAME].relation_changed, self.handle_default_gateway
)
self.framework.observe(self.on["istio-pilot"].relation_changed, self.send_info)
self.framework.observe(self.on['ingress'].relation_changed, self.handle_ingress)
self.framework.observe(self.on['ingress'].relation_broken, self.handle_ingress)
Expand Down Expand Up @@ -126,6 +131,24 @@ def handle_default_gateway(self, event):
# Update the ingress resources as they rely on the default_gateway
self.handle_ingress(event)

# check if gateway is created
self.handle_gateway_relation(event)

def handle_gateway_relation(self, event):
is_gateway_created = self._resource_handler.validate_resource_exist(
resource_type=self._resource_handler.get_custom_resource_class_from_filename(
"gateway.yaml.j2"
),
resource_name=self.model.config['default-gateway'],
resource_namespace=self.model.name,
)
if is_gateway_created:
self.gateway.send_gateway_relation_data(
self.app, self.model.config['default-gateway'], self.model.name
)
else:
self.log.info("Gateway is not created yet. Skip sending gateway relation data.")

def send_info(self, event):
if self.interfaces["istio-pilot"]:
self.interfaces["istio-pilot"].send_data(
Expand Down
21 changes: 21 additions & 0 deletions charms/istio-pilot/src/istio_gateway_name_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import logging
from ops.framework import Object

logger = logging.getLogger(__name__)

DEFAULT_RELATION_NAME = "gateway"


class GatewayProvider(Object):
def __init__(self, charm, relation_name=DEFAULT_RELATION_NAME):
super().__init__(charm, relation_name)

def send_gateway_relation_data(self, charm, gateway_name, gateway_namespace):
relations = self.model.relations["gateway"]
for relation in relations:
relation.data[charm].update(
{
"gateway_name": gateway_name,
"gateway_namespace": gateway_namespace,
}
)
9 changes: 9 additions & 0 deletions charms/istio-pilot/src/resources_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,15 @@ def delete_manifest(
ignore_unauthorized=ignore_unauthorized,
)

def validate_resource_exist(self, resource_type, resource_name, resource_namespace):
try:
self.lightkube_client.get(resource_type, resource_name, namespace=resource_namespace)
return True
except ApiError as error:
self.log.error(str(error))

return False

def get_custom_resource_class_from_filename(self, filename: str):
"""Returns a class representing a namespaced K8s resource.
Expand Down
30 changes: 28 additions & 2 deletions charms/istio-pilot/tests/unit/test_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,34 @@ def test_with_ingress_auth_relation(harness, subprocess, helpers, mocked_client,
assert isinstance(harness.charm.model.unit.status, ActiveStatus)


def test_correct_data_in_gateway_relation(harness, mocker, mocked_client):
harness.set_leader(True)

create_global_resource(
group="networking.istio.io",
version="v1beta1",
kind="Gateway",
plural="gateways",
verbs=None,
)

mocked_validate_gateway = mocker.patch(
"resources_handler.ResourceHandler.validate_resource_exist"
)
mocked_validate_gateway.return_value = True

rel_id = harness.add_relation("gateway", "app")
harness.add_relation_unit(rel_id, "app/0")
harness.set_model_name("test-model")
harness.begin_with_initial_hooks()

gateway_relations = harness.model.relations["gateway"]
istio_relation_data = gateway_relations[0].data[harness.model.app]

assert istio_relation_data["gateway_name"] == harness.model.config["default-gateway"]
assert istio_relation_data["gateway_namespace"] == harness.model.name


def test_removal(harness, subprocess, mocked_client, helpers, mocker):
check_output = subprocess.check_output

Expand Down Expand Up @@ -374,8 +402,6 @@ def test_removal(harness, subprocess, mocked_client, helpers, mocker):
f"values.global.istioNamespace={None}",
]

# Create the gateway resource and emit config_changed
# to create all needed resources
create_global_resource(
group="networking.istio.io",
version="v1beta1",
Expand Down
21 changes: 14 additions & 7 deletions charms/istio-pilot/tox.ini
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
[flake8]
max-line-length = 100

[vars]
src_path = {toxinidir}/src
tst_path = {toxinidir}/tests
all_path = {[vars]src_path} {[vars]tst_path}


[tox]
skipsdist = True

[testenv]
setenv =
PYTHONPATH={toxinidir}/src
PYTHONPATH = {toxinidir}:{toxinidir}/lib:{[vars]src_path}
passenv =
PYTHONPATH
CHARM_BUILD_DIR
MODEL_SETTINGS
deps =
-rtest-requirements.txt
-rrequirements.txt

[testenv:unit]
commands =
pytest tests/unit {posargs}

[vars]
paths = {toxinidir}/src {toxinidir}/tests
pytest {[vars]tst_path}/unit -v --tb native -s {posargs}

[testenv:lint]
commands =
flake8 {[vars]paths}
black --check {[vars]paths}
flake8 {[vars]all_path}
black --check {[vars]all_path}

0 comments on commit 78eb1af

Please sign in to comment.