Add zuuljobs
* Add sunbeam project template to run pep8, py3 tests * Add zuul.d/zuul.yaml to run pep8, py3, cover tests * Update charmcraft and requirements for each charm * Add global tox.ini to invoke fmt, pep8, py3, cover, build * Add gitreview file * Fix py3 test failures in ciner-ceph-k8s, glance-k8s, openstack-exporter * Add jobs for charm builds using files option so that job is invoked if files within the component are modified. Add charm builds to both check and gate pipeline. * Make function tests as part of global. Split the function tests into core, ceph, caas, misc mainly to accomodate function tests to run on 8GB. Add function tests as part of check pipeline. * Add zuul job to publish charms in promote pipeline Add charmhub token as secret that can be used to publish charms. Note: Charmhub token is generated with ttl of 90 days. * Run tox formatting * Make .gitignore, .jujuignore, .stestr.conf global and remove the files from all charms. * Make libs and templates global. Split libs to internal and external so that internal libs can adhere to sunbeam formatting styles. * Add script to copy common files necessary libs, config templates, stestr conf, jujuignore during py3 tests and charm builds. * Tests for keystone-ldap-k8s are commented due to intermittent bug LP#2045206 Change-Id: I804ca64182c109d16bd820ac00f129aa6dcf4496
This commit is contained in:
parent
d3c43c4d9f
commit
df70e376ff
6
charms/aodh-k8s/.gitignore → .gitignore
vendored
6
charms/aodh-k8s/.gitignore → .gitignore
vendored
@ -1,11 +1,11 @@
|
||||
venv/
|
||||
build/
|
||||
.idea/
|
||||
.vscode/
|
||||
*.charm
|
||||
.tox/
|
||||
.coverage
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
.idea
|
||||
.vscode/
|
||||
*.swp
|
||||
**.swp
|
||||
.stestr/
|
@ -1,5 +1,5 @@
|
||||
[gerrit]
|
||||
host=review.opendev.org
|
||||
port=29418
|
||||
project=openstack/charm-heat-k8s.git
|
||||
project=openstack/sunbeam-charms.git
|
||||
defaultbranch=main
|
@ -1,5 +0,0 @@
|
||||
[gerrit]
|
||||
host=review.opendev.org
|
||||
port=29418
|
||||
project=openstack/charm-aodh-k8s.git
|
||||
defaultbranch=main
|
@ -1,11 +0,0 @@
|
||||
- project:
|
||||
templates:
|
||||
- openstack-python3-charm-jobs
|
||||
- openstack-cover-jobs
|
||||
- microk8s-func-test
|
||||
vars:
|
||||
charm_build_name: aodh-k8s
|
||||
juju_channel: 3.1/stable
|
||||
juju_classic_mode: false
|
||||
microk8s_channel: 1.28-strict/stable
|
||||
microk8s_classic_mode: false
|
@ -28,4 +28,3 @@ parts:
|
||||
- jsonschema
|
||||
- pydantic<2.0
|
||||
- jinja2
|
||||
- git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
|
||||
|
@ -1,7 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "INFO: Fetching libs from charmhub."
|
||||
charmcraft fetch-lib charms.data_platform_libs.v0.database_requires
|
||||
charmcraft fetch-lib charms.keystone_k8s.v1.identity_service
|
||||
charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq
|
||||
charmcraft fetch-lib charms.traefik_k8s.v2.ingress
|
@ -1,537 +0,0 @@
|
||||
# Copyright 2023 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.
|
||||
|
||||
r"""[DEPRECATED] Relation 'requires' side abstraction for database relation.
|
||||
|
||||
This library is a uniform interface to a selection of common database
|
||||
metadata, with added custom events that add convenience to database management,
|
||||
and methods to consume the application related data.
|
||||
|
||||
Following an example of using the DatabaseCreatedEvent, in the context of the
|
||||
application charm code:
|
||||
|
||||
```python
|
||||
|
||||
from charms.data_platform_libs.v0.database_requires import (
|
||||
DatabaseCreatedEvent,
|
||||
DatabaseRequires,
|
||||
)
|
||||
|
||||
class ApplicationCharm(CharmBase):
|
||||
# Application charm that connects to database charms.
|
||||
|
||||
def __init__(self, *args):
|
||||
super().__init__(*args)
|
||||
|
||||
# Charm events defined in the database requires charm library.
|
||||
self.database = DatabaseRequires(self, relation_name="database", database_name="database")
|
||||
self.framework.observe(self.database.on.database_created, self._on_database_created)
|
||||
|
||||
def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
|
||||
# Handle the created database
|
||||
|
||||
# Create configuration file for app
|
||||
config_file = self._render_app_config_file(
|
||||
event.username,
|
||||
event.password,
|
||||
event.endpoints,
|
||||
)
|
||||
|
||||
# Start application with rendered configuration
|
||||
self._start_application(config_file)
|
||||
|
||||
# Set active status
|
||||
self.unit.status = ActiveStatus("received database credentials")
|
||||
```
|
||||
|
||||
As shown above, the library provides some custom events to handle specific situations,
|
||||
which are listed below:
|
||||
|
||||
— database_created: event emitted when the requested database is created.
|
||||
— endpoints_changed: event emitted when the read/write endpoints of the database have changed.
|
||||
— read_only_endpoints_changed: event emitted when the read-only endpoints of the database
|
||||
have changed. Event is not triggered if read/write endpoints changed too.
|
||||
|
||||
If it is needed to connect multiple database clusters to the same relation endpoint
|
||||
the application charm can implement the same code as if it would connect to only
|
||||
one database cluster (like the above code example).
|
||||
|
||||
To differentiate multiple clusters connected to the same relation endpoint
|
||||
the application charm can use the name of the remote application:
|
||||
|
||||
```python
|
||||
|
||||
def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
|
||||
# Get the remote app name of the cluster that triggered this event
|
||||
cluster = event.relation.app.name
|
||||
```
|
||||
|
||||
It is also possible to provide an alias for each different database cluster/relation.
|
||||
|
||||
So, it is possible to differentiate the clusters in two ways.
|
||||
The first is to use the remote application name, i.e., `event.relation.app.name`, as above.
|
||||
|
||||
The second way is to use different event handlers to handle each cluster events.
|
||||
The implementation would be something like the following code:
|
||||
|
||||
```python
|
||||
|
||||
from charms.data_platform_libs.v0.database_requires import (
|
||||
DatabaseCreatedEvent,
|
||||
DatabaseRequires,
|
||||
)
|
||||
|
||||
class ApplicationCharm(CharmBase):
|
||||
# Application charm that connects to database charms.
|
||||
|
||||
def __init__(self, *args):
|
||||
super().__init__(*args)
|
||||
|
||||
# Define the cluster aliases and one handler for each cluster database created event.
|
||||
self.database = DatabaseRequires(
|
||||
self,
|
||||
relation_name="database",
|
||||
database_name="database",
|
||||
relations_aliases = ["cluster1", "cluster2"],
|
||||
)
|
||||
self.framework.observe(
|
||||
self.database.on.cluster1_database_created, self._on_cluster1_database_created
|
||||
)
|
||||
self.framework.observe(
|
||||
self.database.on.cluster2_database_created, self._on_cluster2_database_created
|
||||
)
|
||||
|
||||
def _on_cluster1_database_created(self, event: DatabaseCreatedEvent) -> None:
|
||||
# Handle the created database on the cluster named cluster1
|
||||
|
||||
# Create configuration file for app
|
||||
config_file = self._render_app_config_file(
|
||||
event.username,
|
||||
event.password,
|
||||
event.endpoints,
|
||||
)
|
||||
...
|
||||
|
||||
def _on_cluster2_database_created(self, event: DatabaseCreatedEvent) -> None:
|
||||
# Handle the created database on the cluster named cluster2
|
||||
|
||||
# Create configuration file for app
|
||||
config_file = self._render_app_config_file(
|
||||
event.username,
|
||||
event.password,
|
||||
event.endpoints,
|
||||
)
|
||||
...
|
||||
|
||||
```
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from collections import namedtuple
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
from ops.charm import (
|
||||
CharmEvents,
|
||||
RelationChangedEvent,
|
||||
RelationEvent,
|
||||
RelationJoinedEvent,
|
||||
)
|
||||
from ops.framework import EventSource, Object
|
||||
from ops.model import Relation
|
||||
|
||||
# The unique Charmhub library identifier, never change it
|
||||
LIBID = "0241e088ffa9440fb4e3126349b2fb62"
|
||||
|
||||
# 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 = 6
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DatabaseEvent(RelationEvent):
|
||||
"""Base class for database events."""
|
||||
|
||||
@property
|
||||
def endpoints(self) -> Optional[str]:
|
||||
"""Returns a comma separated list of read/write endpoints."""
|
||||
if not self.relation.app:
|
||||
return None
|
||||
|
||||
return self.relation.data[self.relation.app].get("endpoints")
|
||||
|
||||
@property
|
||||
def password(self) -> Optional[str]:
|
||||
"""Returns the password for the created user."""
|
||||
if not self.relation.app:
|
||||
return None
|
||||
|
||||
return self.relation.data[self.relation.app].get("password")
|
||||
|
||||
@property
|
||||
def read_only_endpoints(self) -> Optional[str]:
|
||||
"""Returns a comma separated list of read only endpoints."""
|
||||
if not self.relation.app:
|
||||
return None
|
||||
|
||||
return self.relation.data[self.relation.app].get("read-only-endpoints")
|
||||
|
||||
@property
|
||||
def replset(self) -> Optional[str]:
|
||||
"""Returns the replicaset name.
|
||||
|
||||
MongoDB only.
|
||||
"""
|
||||
if not self.relation.app:
|
||||
return None
|
||||
|
||||
return self.relation.data[self.relation.app].get("replset")
|
||||
|
||||
@property
|
||||
def tls(self) -> Optional[str]:
|
||||
"""Returns whether TLS is configured."""
|
||||
if not self.relation.app:
|
||||
return None
|
||||
|
||||
return self.relation.data[self.relation.app].get("tls")
|
||||
|
||||
@property
|
||||
def tls_ca(self) -> Optional[str]:
|
||||
"""Returns TLS CA."""
|
||||
if not self.relation.app:
|
||||
return None
|
||||
|
||||
return self.relation.data[self.relation.app].get("tls-ca")
|
||||
|
||||
@property
|
||||
def uris(self) -> Optional[str]:
|
||||
"""Returns the connection URIs.
|
||||
|
||||
MongoDB, Redis, OpenSearch and Kafka only.
|
||||
"""
|
||||
if not self.relation.app:
|
||||
return None
|
||||
|
||||
return self.relation.data[self.relation.app].get("uris")
|
||||
|
||||
@property
|
||||
def username(self) -> Optional[str]:
|
||||
"""Returns the created username."""
|
||||
if not self.relation.app:
|
||||
return None
|
||||
|
||||
return self.relation.data[self.relation.app].get("username")
|
||||
|
||||
@property
|
||||
def version(self) -> Optional[str]:
|
||||
"""Returns the version of the database.
|
||||
|
||||
Version as informed by the database daemon.
|
||||
"""
|
||||
if not self.relation.app:
|
||||
return None
|
||||
|
||||
return self.relation.data[self.relation.app].get("version")
|
||||
|
||||
|
||||
class DatabaseCreatedEvent(DatabaseEvent):
|
||||
"""Event emitted when a new database is created for use on this relation."""
|
||||
|
||||
|
||||
class DatabaseEndpointsChangedEvent(DatabaseEvent):
|
||||
"""Event emitted when the read/write endpoints are changed."""
|
||||
|
||||
|
||||
class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent):
|
||||
"""Event emitted when the read only endpoints are changed."""
|
||||
|
||||
|
||||
class DatabaseEvents(CharmEvents):
|
||||
"""Database events.
|
||||
|
||||
This class defines the events that the database can emit.
|
||||
"""
|
||||
|
||||
database_created = EventSource(DatabaseCreatedEvent)
|
||||
endpoints_changed = EventSource(DatabaseEndpointsChangedEvent)
|
||||
read_only_endpoints_changed = EventSource(DatabaseReadOnlyEndpointsChangedEvent)
|
||||
|
||||
|
||||
Diff = namedtuple("Diff", "added changed deleted")
|
||||
Diff.__doc__ = """
|
||||
A tuple for storing the diff between two data mappings.
|
||||
|
||||
— added — keys that were added.
|
||||
— changed — keys that still exist but have new values.
|
||||
— deleted — keys that were deleted.
|
||||
"""
|
||||
|
||||
|
||||
class DatabaseRequires(Object):
|
||||
"""Requires-side of the database relation."""
|
||||
|
||||
on = DatabaseEvents() # pyright: ignore [reportGeneralTypeIssues]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
charm,
|
||||
relation_name: str,
|
||||
database_name: str,
|
||||
extra_user_roles: Optional[str] = None,
|
||||
relations_aliases: Optional[List[str]] = None,
|
||||
):
|
||||
"""Manager of database client relations."""
|
||||
super().__init__(charm, relation_name)
|
||||
self.charm = charm
|
||||
self.database = database_name
|
||||
self.extra_user_roles = extra_user_roles
|
||||
self.local_app = self.charm.model.app
|
||||
self.local_unit = self.charm.unit
|
||||
self.relation_name = relation_name
|
||||
self.relations_aliases = relations_aliases
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_joined, self._on_relation_joined_event
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_changed, self._on_relation_changed_event
|
||||
)
|
||||
|
||||
# Define custom event names for each alias.
|
||||
if relations_aliases:
|
||||
# Ensure the number of aliases does not exceed the maximum
|
||||
# of connections allowed in the specific relation.
|
||||
relation_connection_limit = self.charm.meta.requires[relation_name].limit
|
||||
if len(relations_aliases) != relation_connection_limit:
|
||||
raise ValueError(
|
||||
f"The number of aliases must match the maximum number of connections allowed in the relation. "
|
||||
f"Expected {relation_connection_limit}, got {len(relations_aliases)}"
|
||||
)
|
||||
|
||||
for relation_alias in relations_aliases:
|
||||
self.on.define_event(f"{relation_alias}_database_created", DatabaseCreatedEvent)
|
||||
self.on.define_event(
|
||||
f"{relation_alias}_endpoints_changed", DatabaseEndpointsChangedEvent
|
||||
)
|
||||
self.on.define_event(
|
||||
f"{relation_alias}_read_only_endpoints_changed",
|
||||
DatabaseReadOnlyEndpointsChangedEvent,
|
||||
)
|
||||
|
||||
def _assign_relation_alias(self, relation_id: int) -> None:
|
||||
"""Assigns an alias to a relation.
|
||||
|
||||
This function writes in the unit data bag.
|
||||
|
||||
Args:
|
||||
relation_id: the identifier for a particular relation.
|
||||
"""
|
||||
# If no aliases were provided, return immediately.
|
||||
if not self.relations_aliases:
|
||||
return
|
||||
|
||||
# Return if an alias was already assigned to this relation
|
||||
# (like when there are more than one unit joining the relation).
|
||||
if (
|
||||
self.charm.model.get_relation(self.relation_name, relation_id)
|
||||
.data[self.local_unit]
|
||||
.get("alias")
|
||||
):
|
||||
return
|
||||
|
||||
# Retrieve the available aliases (the ones that weren't assigned to any relation).
|
||||
available_aliases = self.relations_aliases[:]
|
||||
for relation in self.charm.model.relations[self.relation_name]:
|
||||
alias = relation.data[self.local_unit].get("alias")
|
||||
if alias:
|
||||
logger.debug("Alias %s was already assigned to relation %d", alias, relation.id)
|
||||
available_aliases.remove(alias)
|
||||
|
||||
# Set the alias in the unit relation databag of the specific relation.
|
||||
relation = self.charm.model.get_relation(self.relation_name, relation_id)
|
||||
relation.data[self.local_unit].update({"alias": available_aliases[0]})
|
||||
|
||||
def _diff(self, event: RelationChangedEvent) -> Diff:
|
||||
"""Retrieves the diff of the data in the relation changed databag.
|
||||
|
||||
Args:
|
||||
event: relation changed event.
|
||||
|
||||
Returns:
|
||||
a Diff instance containing the added, deleted and changed
|
||||
keys from the event relation databag.
|
||||
"""
|
||||
# Retrieve the old data from the data key in the local unit relation databag.
|
||||
old_data = json.loads(event.relation.data[self.local_unit].get("data", "{}"))
|
||||
# Retrieve the new data from the event relation databag.
|
||||
new_data = (
|
||||
{key: value for key, value in event.relation.data[event.app].items() if key != "data"}
|
||||
if event.app
|
||||
else {}
|
||||
)
|
||||
|
||||
# These are the keys that were added to the databag and triggered this event.
|
||||
added = new_data.keys() - old_data.keys()
|
||||
# These are the keys that were removed from the databag and triggered this event.
|
||||
deleted = old_data.keys() - new_data.keys()
|
||||
# These are the keys that already existed in the databag,
|
||||
# but had their values changed.
|
||||
changed = {
|
||||
key for key in old_data.keys() & new_data.keys() if old_data[key] != new_data[key]
|
||||
}
|
||||
|
||||
# TODO: evaluate the possibility of losing the diff if some error
|
||||
# happens in the charm before the diff is completely checked (DPE-412).
|
||||
# Convert the new_data to a serializable format and save it for a next diff check.
|
||||
event.relation.data[self.local_unit].update({"data": json.dumps(new_data)})
|
||||
|
||||
# Return the diff with all possible changes.
|
||||
return Diff(added, changed, deleted)
|
||||
|
||||
def _emit_aliased_event(self, event: RelationChangedEvent, event_name: str) -> None:
|
||||
"""Emit an aliased event to a particular relation if it has an alias.
|
||||
|
||||
Args:
|
||||
event: the relation changed event that was received.
|
||||
event_name: the name of the event to emit.
|
||||
"""
|
||||
alias = self._get_relation_alias(event.relation.id)
|
||||
if alias:
|
||||
getattr(self.on, f"{alias}_{event_name}").emit(
|
||||
event.relation, app=event.app, unit=event.unit
|
||||
)
|
||||
|
||||
def _get_relation_alias(self, relation_id: int) -> Optional[str]:
|
||||
"""Returns the relation alias.
|
||||
|
||||
Args:
|
||||
relation_id: the identifier for a particular relation.
|
||||
|
||||
Returns:
|
||||
the relation alias or None if the relation was not found.
|
||||
"""
|
||||
for relation in self.charm.model.relations[self.relation_name]:
|
||||
if relation.id == relation_id:
|
||||
return relation.data[self.local_unit].get("alias")
|
||||
return None
|
||||
|
||||
def fetch_relation_data(self) -> dict:
|
||||
"""Retrieves data from relation.
|
||||
|
||||
This function can be used to retrieve data from a relation
|
||||
in the charm code when outside an event callback.
|
||||
|
||||
Returns:
|
||||
a dict of the values stored in the relation data bag
|
||||
for all relation instances (indexed by the relation ID).
|
||||
"""
|
||||
data = {}
|
||||
for relation in self.relations:
|
||||
data[relation.id] = (
|
||||
{key: value for key, value in relation.data[relation.app].items() if key != "data"}
|
||||
if relation.app
|
||||
else {}
|
||||
)
|
||||
return data
|
||||
|
||||
def _update_relation_data(self, relation_id: int, data: dict) -> None:
|
||||
"""Updates a set of key-value pairs in the relation.
|
||||
|
||||
This function writes in the application data bag, therefore,
|
||||
only the leader unit can call it.
|
||||
|
||||
Args:
|
||||
relation_id: the identifier for a particular relation.
|
||||
data: dict containing the key-value pairs
|
||||
that should be updated in the relation.
|
||||
"""
|
||||
if self.local_unit.is_leader():
|
||||
relation = self.charm.model.get_relation(self.relation_name, relation_id)
|
||||
relation.data[self.local_app].update(data)
|
||||
|
||||
def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None:
|
||||
"""Event emitted when the application joins the database relation."""
|
||||
# If relations aliases were provided, assign one to the relation.
|
||||
self._assign_relation_alias(event.relation.id)
|
||||
|
||||
# Sets both database and extra user roles in the relation
|
||||
# if the roles are provided. Otherwise, sets only the database.
|
||||
if self.extra_user_roles:
|
||||
self._update_relation_data(
|
||||
event.relation.id,
|
||||
{
|
||||
"database": self.database,
|
||||
"extra-user-roles": self.extra_user_roles,
|
||||
},
|
||||
)
|
||||
else:
|
||||
self._update_relation_data(event.relation.id, {"database": self.database})
|
||||
|
||||
def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
|
||||
"""Event emitted when the database relation has changed."""
|
||||
# Check which data has changed to emit customs events.
|
||||
diff = self._diff(event)
|
||||
|
||||
# Check if the database is created
|
||||
# (the database charm shared the credentials).
|
||||
if "username" in diff.added and "password" in diff.added:
|
||||
# Emit the default event (the one without an alias).
|
||||
logger.info("database created at %s", datetime.now())
|
||||
getattr(self.on, "database_created").emit(
|
||||
event.relation, app=event.app, unit=event.unit
|
||||
)
|
||||
|
||||
# Emit the aliased event (if any).
|
||||
self._emit_aliased_event(event, "database_created")
|
||||
|
||||
# To avoid unnecessary application restarts do not trigger
|
||||
# “endpoints_changed“ event if “database_created“ is triggered.
|
||||
return
|
||||
|
||||
# Emit an endpoints changed event if the database
|
||||
# added or changed this info in the relation databag.
|
||||
if "endpoints" in diff.added or "endpoints" in diff.changed:
|
||||
# Emit the default event (the one without an alias).
|
||||
logger.info("endpoints changed on %s", datetime.now())
|
||||
getattr(self.on, "endpoints_changed").emit(
|
||||
event.relation, app=event.app, unit=event.unit
|
||||
)
|
||||
|
||||
# Emit the aliased event (if any).
|
||||
self._emit_aliased_event(event, "endpoints_changed")
|
||||
|
||||
# To avoid unnecessary application restarts do not trigger
|
||||
# “read_only_endpoints_changed“ event if “endpoints_changed“ is triggered.
|
||||
return
|
||||
|
||||
# Emit a read only endpoints changed event if the database
|
||||
# added or changed this info in the relation databag.
|
||||
if "read-only-endpoints" in diff.added or "read-only-endpoints" in diff.changed:
|
||||
# Emit the default event (the one without an alias).
|
||||
logger.info("read-only-endpoints changed on %s", datetime.now())
|
||||
getattr(self.on, "read_only_endpoints_changed").emit(
|
||||
event.relation, app=event.app, unit=event.unit
|
||||
)
|
||||
|
||||
# Emit the aliased event (if any).
|
||||
self._emit_aliased_event(event, "read_only_endpoints_changed")
|
||||
|
||||
@property
|
||||
def relations(self) -> List[Relation]:
|
||||
"""The list of Relation instances associated with this relation_name."""
|
||||
return list(self.charm.model.relations[self.relation_name])
|
@ -1,525 +0,0 @@
|
||||
"""IdentityServiceProvides and Requires module.
|
||||
|
||||
|
||||
This library contains the Requires and Provides classes for handling
|
||||
the identity_service interface.
|
||||
|
||||
Import `IdentityServiceRequires` in your charm, with the charm object and the
|
||||
relation name:
|
||||
- self
|
||||
- "identity_service"
|
||||
|
||||
Also provide additional parameters to the charm object:
|
||||
- service
|
||||
- internal_url
|
||||
- public_url
|
||||
- admin_url
|
||||
- region
|
||||
- username
|
||||
- vhost
|
||||
|
||||
Two events are also available to respond to:
|
||||
- connected
|
||||
- ready
|
||||
- goneaway
|
||||
|
||||
A basic example showing the usage of this relation follows:
|
||||
|
||||
```
|
||||
from charms.keystone_k8s.v1.identity_service import IdentityServiceRequires
|
||||
|
||||
class IdentityServiceClientCharm(CharmBase):
|
||||
def __init__(self, *args):
|
||||
super().__init__(*args)
|
||||
# IdentityService Requires
|
||||
self.identity_service = IdentityServiceRequires(
|
||||
self, "identity_service",
|
||||
service = "my-service"
|
||||
internal_url = "http://internal-url"
|
||||
public_url = "http://public-url"
|
||||
admin_url = "http://admin-url"
|
||||
region = "region"
|
||||
)
|
||||
self.framework.observe(
|
||||
self.identity_service.on.connected, self._on_identity_service_connected)
|
||||
self.framework.observe(
|
||||
self.identity_service.on.ready, self._on_identity_service_ready)
|
||||
self.framework.observe(
|
||||
self.identity_service.on.goneaway, self._on_identity_service_goneaway)
|
||||
|
||||
def _on_identity_service_connected(self, event):
|
||||
'''React to the IdentityService connected event.
|
||||
|
||||
This event happens when n IdentityService relation is added to the
|
||||
model before credentials etc have been provided.
|
||||
'''
|
||||
# Do something before the relation is complete
|
||||
pass
|
||||
|
||||
def _on_identity_service_ready(self, event):
|
||||
'''React to the IdentityService ready event.
|
||||
|
||||
The IdentityService interface will use the provided config for the
|
||||
request to the identity server.
|
||||
'''
|
||||
# IdentityService Relation is ready. Do something with the completed relation.
|
||||
pass
|
||||
|
||||
def _on_identity_service_goneaway(self, event):
|
||||
'''React to the IdentityService goneaway event.
|
||||
|
||||
This event happens when an IdentityService relation is removed.
|
||||
'''
|
||||
# IdentityService Relation has goneaway. shutdown services or suchlike
|
||||
pass
|
||||
```
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
from ops.framework import (
|
||||
StoredState,
|
||||
EventBase,
|
||||
ObjectEvents,
|
||||
EventSource,
|
||||
Object,
|
||||
)
|
||||
from ops.model import (
|
||||
Relation,
|
||||
SecretNotFoundError,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# The unique Charmhub library identifier, never change it
|
||||
LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0"
|
||||
|
||||
# Increment this major API version when introducing breaking changes
|
||||
LIBAPI = 1
|
||||
|
||||
# Increment this PATCH version before using `charmcraft publish-lib` or reset
|
||||
# to 0 if you are raising the major API version
|
||||
LIBPATCH = 1
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IdentityServiceConnectedEvent(EventBase):
|
||||
"""IdentityService connected Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class IdentityServiceReadyEvent(EventBase):
|
||||
"""IdentityService ready for use Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class IdentityServiceGoneAwayEvent(EventBase):
|
||||
"""IdentityService relation has gone-away Event"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class IdentityServiceServerEvents(ObjectEvents):
|
||||
"""Events class for `on`"""
|
||||
|
||||
connected = EventSource(IdentityServiceConnectedEvent)
|
||||
ready = EventSource(IdentityServiceReadyEvent)
|
||||
goneaway = EventSource(IdentityServiceGoneAwayEvent)
|
||||
|
||||
|
||||
class IdentityServiceRequires(Object):
|
||||
"""
|
||||
IdentityServiceRequires class
|
||||
"""
|
||||
|
||||
on = IdentityServiceServerEvents()
|
||||
_stored = StoredState()
|
||||
|
||||
def __init__(self, charm, relation_name: str, service_endpoints: dict,
|
||||
region: str):
|
||||
super().__init__(charm, relation_name)
|
||||
self.charm = charm
|
||||
self.relation_name = relation_name
|
||||
self.service_endpoints = service_endpoints
|
||||
self.region = region
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_joined,
|
||||
self._on_identity_service_relation_joined,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_changed,
|
||||
self._on_identity_service_relation_changed,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_departed,
|
||||
self._on_identity_service_relation_changed,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_broken,
|
||||
self._on_identity_service_relation_broken,
|
||||
)
|
||||
|
||||
def _on_identity_service_relation_joined(self, event):
|
||||
"""IdentityService relation joined."""
|
||||
logging.debug("IdentityService on_joined")
|
||||
self.on.connected.emit()
|
||||
self.register_services(
|
||||
self.service_endpoints,
|
||||
self.region)
|
||||
|
||||
def _on_identity_service_relation_changed(self, event):
|
||||
"""IdentityService relation changed."""
|
||||
logging.debug("IdentityService on_changed")
|
||||
try:
|
||||
self.service_password
|
||||
self.on.ready.emit()
|
||||
except (AttributeError, KeyError):
|
||||
pass
|
||||
|
||||
def _on_identity_service_relation_broken(self, event):
|
||||
"""IdentityService relation broken."""
|
||||
logging.debug("IdentityService on_broken")
|
||||
self.on.goneaway.emit()
|
||||
|
||||
@property
|
||||
def _identity_service_rel(self) -> Relation:
|
||||
"""The IdentityService relation."""
|
||||
return self.framework.model.get_relation(self.relation_name)
|
||||
|
||||
def get_remote_app_data(self, key: str) -> str:
|
||||
"""Return the value for the given key from remote app data."""
|
||||
data = self._identity_service_rel.data[self._identity_service_rel.app]
|
||||
return data.get(key)
|
||||
|
||||
@property
|
||||
def api_version(self) -> str:
|
||||
"""Return the api_version."""
|
||||
return self.get_remote_app_data('api-version')
|
||||
|
||||
@property
|
||||
def auth_host(self) -> str:
|
||||
"""Return the auth_host."""
|
||||
return self.get_remote_app_data('auth-host')
|
||||
|
||||
@property
|
||||
def auth_port(self) -> str:
|
||||
"""Return the auth_port."""
|
||||
return self.get_remote_app_data('auth-port')
|
||||
|
||||
@property
|
||||
def auth_protocol(self) -> str:
|
||||
"""Return the auth_protocol."""
|
||||
return self.get_remote_app_data('auth-protocol')
|
||||
|
||||
@property
|
||||
def internal_host(self) -> str:
|
||||
"""Return the internal_host."""
|
||||
return self.get_remote_app_data('internal-host')
|
||||
|
||||
@property
|
||||
def internal_port(self) -> str:
|
||||
"""Return the internal_port."""
|
||||
return self.get_remote_app_data('internal-port')
|
||||
|
||||
@property
|
||||
def internal_protocol(self) -> str:
|
||||
"""Return the internal_protocol."""
|
||||
return self.get_remote_app_data('internal-protocol')
|
||||
|
||||
@property
|
||||
def admin_domain_name(self) -> str:
|
||||
"""Return the admin_domain_name."""
|
||||
return self.get_remote_app_data('admin-domain-name')
|
||||
|
||||
@property
|
||||
def admin_domain_id(self) -> str:
|
||||
"""Return the admin_domain_id."""
|
||||
return self.get_remote_app_data('admin-domain-id')
|
||||
|
||||
@property
|
||||
def admin_project_name(self) -> str:
|
||||
"""Return the admin_project_name."""
|
||||
return self.get_remote_app_data('admin-project-name')
|
||||
|
||||
@property
|
||||
def admin_project_id(self) -> str:
|
||||
"""Return the admin_project_id."""
|
||||
return self.get_remote_app_data('admin-project-id')
|
||||
|
||||
@property
|
||||
def admin_user_name(self) -> str:
|
||||
"""Return the admin_user_name."""
|
||||
return self.get_remote_app_data('admin-user-name')
|
||||
|
||||
@property
|
||||
def admin_user_id(self) -> str:
|
||||
"""Return the admin_user_id."""
|
||||
return self.get_remote_app_data('admin-user-id')
|
||||
|
||||
@property
|
||||
def service_domain_name(self) -> str:
|
||||
"""Return the service_domain_name."""
|
||||
return self.get_remote_app_data('service-domain-name')
|
||||
|
||||
@property
|
||||
def service_domain_id(self) -> str:
|
||||
"""Return the service_domain_id."""
|
||||
return self.get_remote_app_data('service-domain-id')
|
||||
|
||||
@property
|
||||
def service_host(self) -> str:
|
||||
"""Return the service_host."""
|
||||
return self.get_remote_app_data('service-host')
|
||||
|
||||
@property
|
||||
def service_credentials(self) -> str:
|
||||
"""Return the service_credentials secret."""
|
||||
return self.get_remote_app_data('service-credentials')
|
||||
|
||||
@property
|
||||
def service_password(self) -> str:
|
||||
"""Return the service_password."""
|
||||
credentials_id = self.get_remote_app_data('service-credentials')
|
||||
if not credentials_id:
|
||||
return None
|
||||
|
||||
try:
|
||||
credentials = self.charm.model.get_secret(id=credentials_id)
|
||||
return credentials.get_content().get("password")
|
||||
except SecretNotFoundError:
|
||||
logger.warning(f"Secret {credentials_id} not found")
|
||||
return None
|
||||
|
||||
@property
|
||||
def service_port(self) -> str:
|
||||
"""Return the service_port."""
|
||||
return self.get_remote_app_data('service-port')
|
||||
|
||||
@property
|
||||
def service_protocol(self) -> str:
|
||||
"""Return the service_protocol."""
|
||||
return self.get_remote_app_data('service-protocol')
|
||||
|
||||
@property
|
||||
def service_project_name(self) -> str:
|
||||
"""Return the service_project_name."""
|
||||
return self.get_remote_app_data('service-project-name')
|
||||
|
||||
@property
|
||||
def service_project_id(self) -> str:
|
||||
"""Return the service_project_id."""
|
||||
return self.get_remote_app_data('service-project-id')
|
||||
|
||||
@property
|
||||
def service_user_name(self) -> str:
|
||||
"""Return the service_user_name."""
|
||||
credentials_id = self.get_remote_app_data('service-credentials')
|
||||
if not credentials_id:
|
||||
return None
|
||||
|
||||
try:
|
||||
credentials = self.charm.model.get_secret(id=credentials_id)
|
||||
return credentials.get_content().get("username")
|
||||
except SecretNotFoundError:
|
||||
logger.warning(f"Secret {credentials_id} not found")
|
||||
return None
|
||||
|
||||
@property
|
||||
def service_user_id(self) -> str:
|
||||
"""Return the service_user_id."""
|
||||
return self.get_remote_app_data('service-user-id')
|
||||
|
||||
@property
|
||||
def internal_auth_url(self) -> str:
|
||||
"""Return the internal_auth_url."""
|
||||
return self.get_remote_app_data('internal-auth-url')
|
||||
|
||||
@property
|
||||
def admin_auth_url(self) -> str:
|
||||
"""Return the admin_auth_url."""
|
||||
return self.get_remote_app_data('admin-auth-url')
|
||||
|
||||
@property
|
||||
def public_auth_url(self) -> str:
|
||||
"""Return the public_auth_url."""
|
||||
return self.get_remote_app_data('public-auth-url')
|
||||
|
||||
@property
|
||||
def admin_role(self) -> str:
|
||||
"""Return the admin_role."""
|
||||
return self.get_remote_app_data('admin-role')
|
||||
|
||||
def register_services(self, service_endpoints: dict,
|
||||
region: str) -> None:
|
||||
"""Request access to the IdentityService server."""
|
||||
if self.model.unit.is_leader():
|
||||
logging.debug("Requesting service registration")
|
||||
app_data = self._identity_service_rel.data[self.charm.app]
|
||||
app_data["service-endpoints"] = json.dumps(
|
||||
service_endpoints, sort_keys=True
|
||||
)
|
||||
app_data["region"] = region
|
||||
|
||||
|
||||
class HasIdentityServiceClientsEvent(EventBase):
|
||||
"""Has IdentityServiceClients Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ReadyIdentityServiceClientsEvent(EventBase):
|
||||
"""IdentityServiceClients Ready Event."""
|
||||
|
||||
def __init__(self, handle, relation_id, relation_name, service_endpoints,
|
||||
region, client_app_name):
|
||||
super().__init__(handle)
|
||||
self.relation_id = relation_id
|
||||
self.relation_name = relation_name
|
||||
self.service_endpoints = service_endpoints
|
||||
self.region = region
|
||||
self.client_app_name = client_app_name
|
||||
|
||||
def snapshot(self):
|
||||
return {
|
||||
"relation_id": self.relation_id,
|
||||
"relation_name": self.relation_name,
|
||||
"service_endpoints": self.service_endpoints,
|
||||
"client_app_name": self.client_app_name,
|
||||
"region": self.region}
|
||||
|
||||
def restore(self, snapshot):
|
||||
super().restore(snapshot)
|
||||
self.relation_id = snapshot["relation_id"]
|
||||
self.relation_name = snapshot["relation_name"]
|
||||
self.service_endpoints = snapshot["service_endpoints"]
|
||||
self.region = snapshot["region"]
|
||||
self.client_app_name = snapshot["client_app_name"]
|
||||
|
||||
|
||||
class IdentityServiceClientEvents(ObjectEvents):
|
||||
"""Events class for `on`"""
|
||||
|
||||
has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent)
|
||||
ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent)
|
||||
|
||||
|
||||
class IdentityServiceProvides(Object):
|
||||
"""
|
||||
IdentityServiceProvides class
|
||||
"""
|
||||
|
||||
on = IdentityServiceClientEvents()
|
||||
_stored = StoredState()
|
||||
|
||||
def __init__(self, charm, relation_name):
|
||||
super().__init__(charm, relation_name)
|
||||
self.charm = charm
|
||||
self.relation_name = relation_name
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_joined,
|
||||
self._on_identity_service_relation_joined,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_changed,
|
||||
self._on_identity_service_relation_changed,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_broken,
|
||||
self._on_identity_service_relation_broken,
|
||||
)
|
||||
|
||||
def _on_identity_service_relation_joined(self, event):
|
||||
"""Handle IdentityService joined."""
|
||||
logging.debug("IdentityService on_joined")
|
||||
self.on.has_identity_service_clients.emit()
|
||||
|
||||
def _on_identity_service_relation_changed(self, event):
|
||||
"""Handle IdentityService changed."""
|
||||
logging.debug("IdentityService on_changed")
|
||||
REQUIRED_KEYS = [
|
||||
'service-endpoints',
|
||||
'region']
|
||||
|
||||
values = [
|
||||
event.relation.data[event.relation.app].get(k)
|
||||
for k in REQUIRED_KEYS
|
||||
]
|
||||
# Validate data on the relation
|
||||
if all(values):
|
||||
service_eps = json.loads(
|
||||
event.relation.data[event.relation.app]['service-endpoints'])
|
||||
self.on.ready_identity_service_clients.emit(
|
||||
event.relation.id,
|
||||
event.relation.name,
|
||||
service_eps,
|
||||
event.relation.data[event.relation.app]['region'],
|
||||
event.relation.app.name)
|
||||
|
||||
def _on_identity_service_relation_broken(self, event):
|
||||
"""Handle IdentityService broken."""
|
||||
logging.debug("IdentityServiceProvides on_departed")
|
||||
# TODO clear data on the relation
|
||||
|
||||
def set_identity_service_credentials(self, relation_name: int,
|
||||
relation_id: str,
|
||||
api_version: str,
|
||||
auth_host: str,
|
||||
auth_port: str,
|
||||
auth_protocol: str,
|
||||
internal_host: str,
|
||||
internal_port: str,
|
||||
internal_protocol: str,
|
||||
service_host: str,
|
||||
service_port: str,
|
||||
service_protocol: str,
|
||||
admin_domain: str,
|
||||
admin_project: str,
|
||||
admin_user: str,
|
||||
service_domain: str,
|
||||
service_project: str,
|
||||
service_user: str,
|
||||
internal_auth_url: str,
|
||||
admin_auth_url: str,
|
||||
public_auth_url: str,
|
||||
service_credentials: str,
|
||||
admin_role: str):
|
||||
logging.debug("Setting identity_service connection information.")
|
||||
_identity_service_rel = None
|
||||
for relation in self.framework.model.relations[relation_name]:
|
||||
if relation.id == relation_id:
|
||||
_identity_service_rel = relation
|
||||
if not _identity_service_rel:
|
||||
# Relation has disappeared so skip send of data
|
||||
return
|
||||
app_data = _identity_service_rel.data[self.charm.app]
|
||||
app_data["api-version"] = api_version
|
||||
app_data["auth-host"] = auth_host
|
||||
app_data["auth-port"] = str(auth_port)
|
||||
app_data["auth-protocol"] = auth_protocol
|
||||
app_data["internal-host"] = internal_host
|
||||
app_data["internal-port"] = str(internal_port)
|
||||
app_data["internal-protocol"] = internal_protocol
|
||||
app_data["service-host"] = service_host
|
||||
app_data["service-port"] = str(service_port)
|
||||
app_data["service-protocol"] = service_protocol
|
||||
app_data["admin-domain-name"] = admin_domain.name
|
||||
app_data["admin-domain-id"] = admin_domain.id
|
||||
app_data["admin-project-name"] = admin_project.name
|
||||
app_data["admin-project-id"] = admin_project.id
|
||||
app_data["admin-user-name"] = admin_user.name
|
||||
app_data["admin-user-id"] = admin_user.id
|
||||
app_data["service-domain-name"] = service_domain.name
|
||||
app_data["service-domain-id"] = service_domain.id
|
||||
app_data["service-project-name"] = service_project.name
|
||||
app_data["service-project-id"] = service_project.id
|
||||
app_data["service-user-id"] = service_user.id
|
||||
app_data["internal-auth-url"] = internal_auth_url
|
||||
app_data["admin-auth-url"] = admin_auth_url
|
||||
app_data["public-auth-url"] = public_auth_url
|
||||
app_data["service-credentials"] = service_credentials
|
||||
app_data["admin-role"] = admin_role
|
@ -1,408 +0,0 @@
|
||||
# Copyright 2023 Canonical Ltd.
|
||||
# Licensed under the Apache2.0, see LICENCE file in charm source for details.
|
||||
"""Library for the ingress relation.
|
||||
|
||||
This library contains the Requires and Provides classes for handling
|
||||
the ingress interface.
|
||||
|
||||
Import `IngressRequires` in your charm, with two required options:
|
||||
- "self" (the charm itself)
|
||||
- config_dict
|
||||
|
||||
`config_dict` accepts the following keys:
|
||||
- additional-hostnames
|
||||
- backend-protocol
|
||||
- limit-rps
|
||||
- limit-whitelist
|
||||
- max-body-size
|
||||
- owasp-modsecurity-crs
|
||||
- owasp-modsecurity-custom-rules
|
||||
- path-routes
|
||||
- retry-errors
|
||||
- rewrite-enabled
|
||||
- rewrite-target
|
||||
- service-hostname (required)
|
||||
- service-name (required)
|
||||
- service-namespace
|
||||
- service-port (required)
|
||||
- session-cookie-max-age
|
||||
- tls-secret-name
|
||||
|
||||
See [the config section](https://charmhub.io/nginx-ingress-integrator/configure) for descriptions
|
||||
of each, along with the required type.
|
||||
|
||||
As an example, add the following to `src/charm.py`:
|
||||
```
|
||||
from charms.nginx_ingress_integrator.v0.ingress import IngressRequires
|
||||
|
||||
# In your charm's `__init__` method.
|
||||
self.ingress = IngressRequires(self, {
|
||||
"service-hostname": self.config["external_hostname"],
|
||||
"service-name": self.app.name,
|
||||
"service-port": 80,
|
||||
}
|
||||
)
|
||||
|
||||
# In your charm's `config-changed` handler.
|
||||
self.ingress.update_config({"service-hostname": self.config["external_hostname"]})
|
||||
```
|
||||
And then add the following to `metadata.yaml`:
|
||||
```
|
||||
requires:
|
||||
ingress:
|
||||
interface: ingress
|
||||
```
|
||||
You _must_ register the IngressRequires class as part of the `__init__` method
|
||||
rather than, for instance, a config-changed event handler, for the relation
|
||||
changed event to be properly handled.
|
||||
"""
|
||||
|
||||
import copy
|
||||
import logging
|
||||
from typing import Dict
|
||||
|
||||
from ops.charm import CharmBase, CharmEvents, RelationBrokenEvent, RelationChangedEvent
|
||||
from ops.framework import EventBase, EventSource, Object
|
||||
from ops.model import BlockedStatus
|
||||
|
||||
INGRESS_RELATION_NAME = "ingress"
|
||||
INGRESS_PROXY_RELATION_NAME = "ingress-proxy"
|
||||
|
||||
# The unique Charmhub library identifier, never change it
|
||||
LIBID = "db0af4367506491c91663468fb5caa4c"
|
||||
|
||||
# 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 = 16
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIRED_INGRESS_RELATION_FIELDS = {"service-hostname", "service-name", "service-port"}
|
||||
|
||||
OPTIONAL_INGRESS_RELATION_FIELDS = {
|
||||
"additional-hostnames",
|
||||
"backend-protocol",
|
||||
"limit-rps",
|
||||
"limit-whitelist",
|
||||
"max-body-size",
|
||||
"owasp-modsecurity-crs",
|
||||
"owasp-modsecurity-custom-rules",
|
||||
"path-routes",
|
||||
"retry-errors",
|
||||
"rewrite-target",
|
||||
"rewrite-enabled",
|
||||
"service-namespace",
|
||||
"session-cookie-max-age",
|
||||
"tls-secret-name",
|
||||
}
|
||||
|
||||
RELATION_INTERFACES_MAPPINGS = {
|
||||
"service-hostname": "host",
|
||||
"service-name": "name",
|
||||
"service-namespace": "model",
|
||||
"service-port": "port",
|
||||
}
|
||||
RELATION_INTERFACES_MAPPINGS_VALUES = set(RELATION_INTERFACES_MAPPINGS.values())
|
||||
|
||||
|
||||
class IngressAvailableEvent(EventBase):
|
||||
"""IngressAvailableEvent custom event.
|
||||
|
||||
This event indicates the Ingress provider is available.
|
||||
"""
|
||||
|
||||
|
||||
class IngressProxyAvailableEvent(EventBase):
|
||||
"""IngressProxyAvailableEvent custom event.
|
||||
|
||||
This event indicates the IngressProxy provider is available.
|
||||
"""
|
||||
|
||||
|
||||
class IngressBrokenEvent(RelationBrokenEvent):
|
||||
"""IngressBrokenEvent custom event.
|
||||
|
||||
This event indicates the Ingress provider is broken.
|
||||
"""
|
||||
|
||||
|
||||
class IngressCharmEvents(CharmEvents):
|
||||
"""Custom charm events.
|
||||
|
||||
Attrs:
|
||||
ingress_available: Event to indicate that Ingress is available.
|
||||
ingress_proxy_available: Event to indicate that IngressProxy is available.
|
||||
ingress_broken: Event to indicate that Ingress is broken.
|
||||
"""
|
||||
|
||||
ingress_available = EventSource(IngressAvailableEvent)
|
||||
ingress_proxy_available = EventSource(IngressProxyAvailableEvent)
|
||||
ingress_broken = EventSource(IngressBrokenEvent)
|
||||
|
||||
|
||||
class IngressRequires(Object):
|
||||
"""This class defines the functionality for the 'requires' side of the 'ingress' relation.
|
||||
|
||||
Hook events observed:
|
||||
- relation-changed
|
||||
|
||||
Attrs:
|
||||
model: Juju model where the charm is deployed.
|
||||
config_dict: Contains all the configuration options for Ingress.
|
||||
"""
|
||||
|
||||
def __init__(self, charm: CharmBase, config_dict: Dict) -> None:
|
||||
"""Init function for the IngressRequires class.
|
||||
|
||||
Args:
|
||||
charm: The charm that requires the ingress relation.
|
||||
config_dict: Contains all the configuration options for Ingress.
|
||||
"""
|
||||
super().__init__(charm, INGRESS_RELATION_NAME)
|
||||
|
||||
self.framework.observe(
|
||||
charm.on[INGRESS_RELATION_NAME].relation_changed, self._on_relation_changed
|
||||
)
|
||||
|
||||
# Set default values.
|
||||
default_relation_fields = {
|
||||
"service-namespace": self.model.name,
|
||||
}
|
||||
config_dict.update(
|
||||
(key, value)
|
||||
for key, value in default_relation_fields.items()
|
||||
if key not in config_dict or not config_dict[key]
|
||||
)
|
||||
|
||||
self.config_dict = self._convert_to_relation_interface(config_dict)
|
||||
|
||||
@staticmethod
|
||||
def _convert_to_relation_interface(config_dict: Dict) -> Dict:
|
||||
"""Create a new relation dict that conforms with charm-relation-interfaces.
|
||||
|
||||
Args:
|
||||
config_dict: Ingress configuration that doesn't conform with charm-relation-interfaces.
|
||||
|
||||
Returns:
|
||||
The Ingress configuration conforming with charm-relation-interfaces.
|
||||
"""
|
||||
config_dict = copy.copy(config_dict)
|
||||
config_dict.update(
|
||||
(key, config_dict[old_key])
|
||||
for old_key, key in RELATION_INTERFACES_MAPPINGS.items()
|
||||
if old_key in config_dict and config_dict[old_key]
|
||||
)
|
||||
return config_dict
|
||||
|
||||
def _config_dict_errors(self, config_dict: Dict, update_only: bool = False) -> bool:
|
||||
"""Check our config dict for errors.
|
||||
|
||||
Args:
|
||||
config_dict: Contains all the configuration options for Ingress.
|
||||
update_only: If the charm needs to update only existing keys.
|
||||
|
||||
Returns:
|
||||
If we need to update the config dict or not.
|
||||
"""
|
||||
blocked_message = "Error in ingress relation, check `juju debug-log`"
|
||||
unknown = [
|
||||
config_key
|
||||
for config_key in config_dict
|
||||
if config_key
|
||||
not in REQUIRED_INGRESS_RELATION_FIELDS
|
||||
| OPTIONAL_INGRESS_RELATION_FIELDS
|
||||
| RELATION_INTERFACES_MAPPINGS_VALUES
|
||||
]
|
||||
if unknown:
|
||||
LOGGER.error(
|
||||
"Ingress relation error, unknown key(s) in config dictionary found: %s",
|
||||
", ".join(unknown),
|
||||
)
|
||||
self.model.unit.status = BlockedStatus(blocked_message)
|
||||
return True
|
||||
if not update_only:
|
||||
missing = tuple(
|
||||
config_key
|
||||
for config_key in REQUIRED_INGRESS_RELATION_FIELDS
|
||||
if config_key not in self.config_dict
|
||||
)
|
||||
if missing:
|
||||
LOGGER.error(
|
||||
"Ingress relation error, missing required key(s) in config dictionary: %s",
|
||||
", ".join(sorted(missing)),
|
||||
)
|
||||
self.model.unit.status = BlockedStatus(blocked_message)
|
||||
return True
|
||||
return False
|
||||
|
||||
def _on_relation_changed(self, event: RelationChangedEvent) -> None:
|
||||
"""Handle the relation-changed event.
|
||||
|
||||
Args:
|
||||
event: Event triggering the relation-changed hook for the relation.
|
||||
"""
|
||||
# `self.unit` isn't available here, so use `self.model.unit`.
|
||||
if self.model.unit.is_leader():
|
||||
if self._config_dict_errors(config_dict=self.config_dict):
|
||||
return
|
||||
event.relation.data[self.model.app].update(
|
||||
(key, str(self.config_dict[key])) for key in self.config_dict
|
||||
)
|
||||
|
||||
def update_config(self, config_dict: Dict) -> None:
|
||||
"""Allow for updates to relation.
|
||||
|
||||
Args:
|
||||
config_dict: Contains all the configuration options for Ingress.
|
||||
|
||||
Attrs:
|
||||
config_dict: Contains all the configuration options for Ingress.
|
||||
"""
|
||||
if self.model.unit.is_leader():
|
||||
self.config_dict = self._convert_to_relation_interface(config_dict)
|
||||
if self._config_dict_errors(self.config_dict, update_only=True):
|
||||
return
|
||||
relation = self.model.get_relation(INGRESS_RELATION_NAME)
|
||||
if relation:
|
||||
for key in self.config_dict:
|
||||
relation.data[self.model.app][key] = str(self.config_dict[key])
|
||||
|
||||
|
||||
class IngressBaseProvides(Object):
|
||||
"""Parent class for IngressProvides and IngressProxyProvides.
|
||||
|
||||
Attrs:
|
||||
model: Juju model where the charm is deployed.
|
||||
"""
|
||||
|
||||
def __init__(self, charm: CharmBase, relation_name: str) -> None:
|
||||
"""Init function for the IngressProxyProvides class.
|
||||
|
||||
Args:
|
||||
charm: The charm that provides the ingress-proxy relation.
|
||||
"""
|
||||
super().__init__(charm, relation_name)
|
||||
self.charm = charm
|
||||
|
||||
def _on_relation_changed(self, event: RelationChangedEvent) -> None:
|
||||
"""Handle a change to the ingress/ingress-proxy relation.
|
||||
|
||||
Confirm we have the fields we expect to receive.
|
||||
|
||||
Args:
|
||||
event: Event triggering the relation-changed hook for the relation.
|
||||
"""
|
||||
# `self.unit` isn't available here, so use `self.model.unit`.
|
||||
if not self.model.unit.is_leader():
|
||||
return
|
||||
|
||||
relation_name = event.relation.name
|
||||
|
||||
assert event.app is not None # nosec
|
||||
if not event.relation.data[event.app]:
|
||||
LOGGER.info(
|
||||
"%s hasn't finished configuring, waiting until relation is changed again.",
|
||||
relation_name,
|
||||
)
|
||||
return
|
||||
|
||||
ingress_data = {
|
||||
field: event.relation.data[event.app].get(field)
|
||||
for field in REQUIRED_INGRESS_RELATION_FIELDS | OPTIONAL_INGRESS_RELATION_FIELDS
|
||||
}
|
||||
|
||||
missing_fields = sorted(
|
||||
field for field in REQUIRED_INGRESS_RELATION_FIELDS if ingress_data.get(field) is None
|
||||
)
|
||||
|
||||
if missing_fields:
|
||||
LOGGER.warning(
|
||||
"Missing required data fields for %s relation: %s",
|
||||
relation_name,
|
||||
", ".join(missing_fields),
|
||||
)
|
||||
self.model.unit.status = BlockedStatus(
|
||||
f"Missing fields for {relation_name}: {', '.join(missing_fields)}"
|
||||
)
|
||||
|
||||
if relation_name == INGRESS_RELATION_NAME:
|
||||
# Conform to charm-relation-interfaces.
|
||||
if "name" in ingress_data and "port" in ingress_data:
|
||||
name = ingress_data["name"]
|
||||
port = ingress_data["port"]
|
||||
else:
|
||||
name = ingress_data["service-name"]
|
||||
port = ingress_data["service-port"]
|
||||
event.relation.data[self.model.app]["url"] = f"http://{name}:{port}/"
|
||||
|
||||
# Create an event that our charm can use to decide it's okay to
|
||||
# configure the ingress.
|
||||
self.charm.on.ingress_available.emit()
|
||||
elif relation_name == INGRESS_PROXY_RELATION_NAME:
|
||||
self.charm.on.ingress_proxy_available.emit()
|
||||
|
||||
|
||||
class IngressProvides(IngressBaseProvides):
|
||||
"""Class containing the functionality for the 'provides' side of the 'ingress' relation.
|
||||
|
||||
Attrs:
|
||||
charm: The charm that provides the ingress relation.
|
||||
|
||||
Hook events observed:
|
||||
- relation-changed
|
||||
"""
|
||||
|
||||
def __init__(self, charm: CharmBase) -> None:
|
||||
"""Init function for the IngressProvides class.
|
||||
|
||||
Args:
|
||||
charm: The charm that provides the ingress relation.
|
||||
"""
|
||||
super().__init__(charm, INGRESS_RELATION_NAME)
|
||||
# Observe the relation-changed hook event and bind
|
||||
# self.on_relation_changed() to handle the event.
|
||||
self.framework.observe(
|
||||
charm.on[INGRESS_RELATION_NAME].relation_changed, self._on_relation_changed
|
||||
)
|
||||
self.framework.observe(
|
||||
charm.on[INGRESS_RELATION_NAME].relation_broken, self._on_relation_broken
|
||||
)
|
||||
|
||||
def _on_relation_broken(self, event: RelationBrokenEvent) -> None:
|
||||
"""Handle a relation-broken event in the ingress relation.
|
||||
|
||||
Args:
|
||||
event: Event triggering the relation-broken hook for the relation.
|
||||
"""
|
||||
if not self.model.unit.is_leader():
|
||||
return
|
||||
|
||||
# Create an event that our charm can use to remove the ingress resource.
|
||||
self.charm.on.ingress_broken.emit(event.relation)
|
||||
|
||||
|
||||
class IngressProxyProvides(IngressBaseProvides):
|
||||
"""Class containing the functionality for the 'provides' side of the 'ingress-proxy' relation.
|
||||
|
||||
Attrs:
|
||||
charm: The charm that provides the ingress-proxy relation.
|
||||
|
||||
Hook events observed:
|
||||
- relation-changed
|
||||
"""
|
||||
|
||||
def __init__(self, charm: CharmBase) -> None:
|
||||
"""Init function for the IngressProxyProvides class.
|
||||
|
||||
Args:
|
||||
charm: The charm that provides the ingress-proxy relation.
|
||||
"""
|
||||
super().__init__(charm, INGRESS_PROXY_RELATION_NAME)
|
||||
# Observe the relation-changed hook event and bind
|
||||
# self.on_relation_changed() to handle the event.
|
||||
self.framework.observe(
|
||||
charm.on[INGRESS_PROXY_RELATION_NAME].relation_changed, self._on_relation_changed
|
||||
)
|
@ -1,10 +0,0 @@
|
||||
- project:
|
||||
templates:
|
||||
- charm-publish-jobs
|
||||
vars:
|
||||
needs_charm_build: true
|
||||
charm_build_name: aodh-k8s
|
||||
build_type: charmcraft
|
||||
publish_charm: true
|
||||
charmcraft_channel: 2.0/stable
|
||||
publish_channel: 2023.2/edge
|
@ -1,13 +0,0 @@
|
||||
#!/bin/bash
|
||||
charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}')
|
||||
echo "renaming ${charm}_*.charm to ${charm}.charm"
|
||||
echo -n "pwd: "
|
||||
pwd
|
||||
ls -al
|
||||
echo "Removing bad downloaded charm maybe?"
|
||||
if [[ -e "${charm}.charm" ]];
|
||||
then
|
||||
rm "${charm}.charm"
|
||||
fi
|
||||
echo "Renaming charm here."
|
||||
mv ${charm}_*.charm ${charm}.charm
|
@ -1,9 +1,11 @@
|
||||
ops
|
||||
jinja2
|
||||
git+https://github.com/openstack/charm-ops-sunbeam#egg=ops_sunbeam
|
||||
lightkube
|
||||
pydantic<2.0
|
||||
|
||||
# Uncomment below if charm relates to ceph
|
||||
# git+https://github.com/openstack/charm-ops-interface-ceph-client#egg=interface_ceph_client
|
||||
# git+https://github.com/juju/charm-helpers.git#egg=charmhelpers
|
||||
|
||||
# From ops_sunbeam
|
||||
tenacity
|
||||
|
@ -1,15 +0,0 @@
|
||||
{% if identity_service.service_domain_id -%}
|
||||
[service_user]
|
||||
{% if identity_service.internal_auth_url -%}
|
||||
auth_url = {{ identity_service.internal_auth_url }}
|
||||
{% elif identity_service.internal_host -%}
|
||||
auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
|
||||
{% endif -%}
|
||||
send_service_user_token = true
|
||||
auth_type = password
|
||||
project_domain_id = {{ identity_service.service_domain_id }}
|
||||
user_domain_id = {{ identity_service.service_domain_id }}
|
||||
project_name = {{ identity_service.service_project_name }}
|
||||
username = {{ identity_service.service_user_name }}
|
||||
password = {{ identity_service.service_password }}
|
||||
{% endif -%}
|
@ -1,11 +0,0 @@
|
||||
# This file is managed centrally. If you find the need to modify this as a
|
||||
# one-off, please don't. Intead, consult #openstack-charms and ask about
|
||||
# requirements management in charms via bot-control. Thank you.
|
||||
|
||||
coverage
|
||||
mock
|
||||
flake8
|
||||
stestr
|
||||
ops
|
||||
# Subunit 1.4.3+ requires extras
|
||||
extras
|
@ -16,9 +16,8 @@
|
||||
|
||||
"""Tests for gnocchi charm."""
|
||||
|
||||
import ops_sunbeam.test_utils as test_utils
|
||||
|
||||
import charm
|
||||
import ops_sunbeam.test_utils as test_utils
|
||||
|
||||
|
||||
class _AodhOperatorCharm(charm.AodhOperatorCharm):
|
||||
|
@ -1,165 +0,0 @@
|
||||
# Operator charm (with zaza): tox.ini
|
||||
|
||||
[tox]
|
||||
skipsdist = True
|
||||
envlist = pep8,py3
|
||||
sitepackages = False
|
||||
skip_missing_interpreters = False
|
||||
minversion = 3.18.0
|
||||
|
||||
[vars]
|
||||
src_path = {toxinidir}/src/
|
||||
tst_path = {toxinidir}/tests/
|
||||
lib_path = {toxinidir}/lib/
|
||||
pyproject_toml = {toxinidir}/pyproject.toml
|
||||
all_path = {[vars]src_path} {[vars]tst_path}
|
||||
|
||||
[testenv]
|
||||
basepython = python3
|
||||
setenv =
|
||||
PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path}
|
||||
passenv =
|
||||
HOME
|
||||
PYTHONPATH
|
||||
install_command =
|
||||
pip install {opts} {packages}
|
||||
commands = stestr run --slowest {posargs}
|
||||
allowlist_externals =
|
||||
git
|
||||
charmcraft
|
||||
{toxinidir}/fetch-libs.sh
|
||||
{toxinidir}/rename.sh
|
||||
deps =
|
||||
-r{toxinidir}/test-requirements.txt
|
||||
|
||||
[testenv:fmt]
|
||||
description = Apply coding style standards to code
|
||||
deps =
|
||||
black
|
||||
isort
|
||||
commands =
|
||||
isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox
|
||||
black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path}
|
||||
|
||||
[testenv:build]
|
||||
basepython = python3
|
||||
deps =
|
||||
commands =
|
||||
charmcraft -v pack
|
||||
{toxinidir}/rename.sh
|
||||
|
||||
[testenv:fetch]
|
||||
basepython = python3
|
||||
deps =
|
||||
commands =
|
||||
{toxinidir}/fetch-libs.sh
|
||||
|
||||
[testenv:py3]
|
||||
basepython = python3
|
||||
deps =
|
||||
{[testenv]deps}
|
||||
-r{toxinidir}/requirements.txt
|
||||
|
||||
[testenv:py38]
|
||||
basepython = python3.8
|
||||
deps = {[testenv:py3]deps}
|
||||
|
||||
[testenv:py39]
|
||||
basepython = python3.9
|
||||
deps = {[testenv:py3]deps}
|
||||
|
||||
[testenv:py310]
|
||||
basepython = python3.10
|
||||
deps = {[testenv:py3]deps}
|
||||
|
||||
[testenv:py311]
|
||||
basepython = python3.11
|
||||
deps = {[testenv:py3]deps}
|
||||
|
||||
[testenv:cover]
|
||||
basepython = python3
|
||||
deps = {[testenv:py3]deps}
|
||||
setenv =
|
||||
{[testenv]setenv}
|
||||
PYTHON=coverage run
|
||||
commands =
|
||||
coverage erase
|
||||
stestr run --slowest {posargs}
|
||||
coverage combine
|
||||
coverage html -d cover
|
||||
coverage xml -o cover/coverage.xml
|
||||
coverage report
|
||||
|
||||
[testenv:pep8]
|
||||
description = Alias for lint
|
||||
deps = {[testenv:lint]deps}
|
||||
commands = {[testenv:lint]commands}
|
||||
|
||||
[testenv:lint]
|
||||
description = Check code against coding style standards
|
||||
deps =
|
||||
black
|
||||
flake8<6 # Pin version until https://github.com/savoirfairelinux/flake8-copyright/issues/19 is merged
|
||||
flake8-docstrings
|
||||
flake8-copyright
|
||||
flake8-builtins
|
||||
pyproject-flake8
|
||||
pep8-naming
|
||||
isort
|
||||
codespell
|
||||
commands =
|
||||
codespell {[vars]all_path}
|
||||
# pflake8 wrapper supports config from pyproject.toml
|
||||
pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path}
|
||||
isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path}
|
||||
black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path}
|
||||
|
||||
[testenv:func-noop]
|
||||
basepython = python3
|
||||
deps =
|
||||
git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza
|
||||
git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack
|
||||
git+https://opendev.org/openstack/tempest.git#egg=tempest
|
||||
commands =
|
||||
functest-run-suite --help
|
||||
|
||||
[testenv:func]
|
||||
basepython = python3
|
||||
deps = {[testenv:func-noop]deps}
|
||||
commands =
|
||||
functest-run-suite --keep-model
|
||||
|
||||
[testenv:func-smoke]
|
||||
basepython = python3
|
||||
deps = {[testenv:func-noop]deps}
|
||||
setenv =
|
||||
TEST_MODEL_SETTINGS = automatically-retry-hooks=true
|
||||
TEST_MAX_RESOLVE_COUNT = 5
|
||||
commands =
|
||||
functest-run-suite --keep-model --smoke
|
||||
|
||||
[testenv:func-dev]
|
||||
basepython = python3
|
||||
deps = {[testenv:func-noop]deps}
|
||||
commands =
|
||||
functest-run-suite --keep-model --dev
|
||||
|
||||
[testenv:func-target]
|
||||
basepython = python3
|
||||
deps = {[testenv:func-noop]deps}
|
||||
commands =
|
||||
functest-run-suite --keep-model --bundle {posargs}
|
||||
|
||||
[coverage:run]
|
||||
branch = True
|
||||
concurrency = multiprocessing
|
||||
parallel = True
|
||||
source =
|
||||
.
|
||||
omit =
|
||||
.tox/*
|
||||
tests/*
|
||||
src/templates/*
|
||||
|
||||
[flake8]
|
||||
ignore=E226,W504
|
@ -1,9 +0,0 @@
|
||||
[flake8]
|
||||
max-line-length = 99
|
||||
select: E,W,F,C,N
|
||||
exclude:
|
||||
venv
|
||||
.git
|
||||
build
|
||||
dist
|
||||
*.egg_info
|
11
charms/barbican-k8s/.gitignore
vendored
11
charms/barbican-k8s/.gitignore
vendored
@ -1,11 +0,0 @@
|
||||
venv/
|
||||
build/
|
||||
*.charm
|
||||
*.swp
|
||||
|
||||
.coverage
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
.tox
|
||||
.stestr/
|
||||
tempest.log
|
@ -1,5 +0,0 @@
|
||||
[gerrit]
|
||||
host=review.opendev.org
|
||||
port=29418
|
||||
project=openstack/charm-barbican-k8s.git
|
||||
defaultbranch=main
|
@ -1,3 +0,0 @@
|
||||
/venv
|
||||
*.py[cod]
|
||||
*.charm
|
@ -1,3 +0,0 @@
|
||||
[DEFAULT]
|
||||
test_path=./tests/unit
|
||||
top_dir=./tests
|
@ -1,11 +0,0 @@
|
||||
- project:
|
||||
templates:
|
||||
- openstack-python3-charm-jobs
|
||||
- openstack-cover-jobs
|
||||
- microk8s-func-test
|
||||
vars:
|
||||
charm_build_name: barbican-k8s
|
||||
juju_channel: 3.2/stable
|
||||
juju_classic_mode: false
|
||||
microk8s_channel: 1.28-strict/stable
|
||||
microk8s_classic_mode: false
|
@ -20,4 +20,3 @@ parts:
|
||||
- jsonschema
|
||||
- pydantic<2.0
|
||||
- jinja2
|
||||
- git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
|
||||
|
@ -1,8 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "INFO: Fetching libs from charmhub."
|
||||
charmcraft fetch-lib charms.data_platform_libs.v0.database_requires
|
||||
charmcraft fetch-lib charms.keystone_k8s.v1.identity_service
|
||||
charmcraft fetch-lib charms.keystone_k8s.v0.identity_resource
|
||||
charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq
|
||||
charmcraft fetch-lib charms.traefik_k8s.v2.ingress
|
@ -1,537 +0,0 @@
|
||||
# Copyright 2023 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.
|
||||
|
||||
r"""[DEPRECATED] Relation 'requires' side abstraction for database relation.
|
||||
|
||||
This library is a uniform interface to a selection of common database
|
||||
metadata, with added custom events that add convenience to database management,
|
||||
and methods to consume the application related data.
|
||||
|
||||
Following an example of using the DatabaseCreatedEvent, in the context of the
|
||||
application charm code:
|
||||
|
||||
```python
|
||||
|
||||
from charms.data_platform_libs.v0.database_requires import (
|
||||
DatabaseCreatedEvent,
|
||||
DatabaseRequires,
|
||||
)
|
||||
|
||||
class ApplicationCharm(CharmBase):
|
||||
# Application charm that connects to database charms.
|
||||
|
||||
def __init__(self, *args):
|
||||
super().__init__(*args)
|
||||
|
||||
# Charm events defined in the database requires charm library.
|
||||
self.database = DatabaseRequires(self, relation_name="database", database_name="database")
|
||||
self.framework.observe(self.database.on.database_created, self._on_database_created)
|
||||
|
||||
def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
|
||||
# Handle the created database
|
||||
|
||||
# Create configuration file for app
|
||||
config_file = self._render_app_config_file(
|
||||
event.username,
|
||||
event.password,
|
||||
event.endpoints,
|
||||
)
|
||||
|
||||
# Start application with rendered configuration
|
||||
self._start_application(config_file)
|
||||
|
||||
# Set active status
|
||||
self.unit.status = ActiveStatus("received database credentials")
|
||||
```
|
||||
|
||||
As shown above, the library provides some custom events to handle specific situations,
|
||||
which are listed below:
|
||||
|
||||
— database_created: event emitted when the requested database is created.
|
||||
— endpoints_changed: event emitted when the read/write endpoints of the database have changed.
|
||||
— read_only_endpoints_changed: event emitted when the read-only endpoints of the database
|
||||
have changed. Event is not triggered if read/write endpoints changed too.
|
||||
|
||||
If it is needed to connect multiple database clusters to the same relation endpoint
|
||||
the application charm can implement the same code as if it would connect to only
|
||||
one database cluster (like the above code example).
|
||||
|
||||
To differentiate multiple clusters connected to the same relation endpoint
|
||||
the application charm can use the name of the remote application:
|
||||
|
||||
```python
|
||||
|
||||
def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
|
||||
# Get the remote app name of the cluster that triggered this event
|
||||
cluster = event.relation.app.name
|
||||
```
|
||||
|
||||
It is also possible to provide an alias for each different database cluster/relation.
|
||||
|
||||
So, it is possible to differentiate the clusters in two ways.
|
||||
The first is to use the remote application name, i.e., `event.relation.app.name`, as above.
|
||||
|
||||
The second way is to use different event handlers to handle each cluster events.
|
||||
The implementation would be something like the following code:
|
||||
|
||||
```python
|
||||
|
||||
from charms.data_platform_libs.v0.database_requires import (
|
||||
DatabaseCreatedEvent,
|
||||
DatabaseRequires,
|
||||
)
|
||||
|
||||
class ApplicationCharm(CharmBase):
|
||||
# Application charm that connects to database charms.
|
||||
|
||||
def __init__(self, *args):
|
||||
super().__init__(*args)
|
||||
|
||||
# Define the cluster aliases and one handler for each cluster database created event.
|
||||
self.database = DatabaseRequires(
|
||||
self,
|
||||
relation_name="database",
|
||||
database_name="database",
|
||||
relations_aliases = ["cluster1", "cluster2"],
|
||||
)
|
||||
self.framework.observe(
|
||||
self.database.on.cluster1_database_created, self._on_cluster1_database_created
|
||||
)
|
||||
self.framework.observe(
|
||||
self.database.on.cluster2_database_created, self._on_cluster2_database_created
|
||||
)
|
||||
|
||||
def _on_cluster1_database_created(self, event: DatabaseCreatedEvent) -> None:
|
||||
# Handle the created database on the cluster named cluster1
|
||||
|
||||
# Create configuration file for app
|
||||
config_file = self._render_app_config_file(
|
||||
event.username,
|
||||
event.password,
|
||||
event.endpoints,
|
||||
)
|
||||
...
|
||||
|
||||
def _on_cluster2_database_created(self, event: DatabaseCreatedEvent) -> None:
|
||||
# Handle the created database on the cluster named cluster2
|
||||
|
||||
# Create configuration file for app
|
||||
config_file = self._render_app_config_file(
|
||||
event.username,
|
||||
event.password,
|
||||
event.endpoints,
|
||||
)
|
||||
...
|
||||
|
||||
```
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from collections import namedtuple
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
from ops.charm import (
|
||||
CharmEvents,
|
||||
RelationChangedEvent,
|
||||
RelationEvent,
|
||||
RelationJoinedEvent,
|
||||
)
|
||||
from ops.framework import EventSource, Object
|
||||
from ops.model import Relation
|
||||
|
||||
# The unique Charmhub library identifier, never change it
|
||||
LIBID = "0241e088ffa9440fb4e3126349b2fb62"
|
||||
|
||||
# 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 = 6
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DatabaseEvent(RelationEvent):
|
||||
"""Base class for database events."""
|
||||
|
||||
@property
|
||||
def endpoints(self) -> Optional[str]:
|
||||
"""Returns a comma separated list of read/write endpoints."""
|
||||
if not self.relation.app:
|
||||
return None
|
||||
|
||||
return self.relation.data[self.relation.app].get("endpoints")
|
||||
|
||||
@property
|
||||
def password(self) -> Optional[str]:
|
||||
"""Returns the password for the created user."""
|
||||
if not self.relation.app:
|
||||
return None
|
||||
|
||||
return self.relation.data[self.relation.app].get("password")
|
||||
|
||||
@property
|
||||
def read_only_endpoints(self) -> Optional[str]:
|
||||
"""Returns a comma separated list of read only endpoints."""
|
||||
if not self.relation.app:
|
||||
return None
|
||||
|
||||
return self.relation.data[self.relation.app].get("read-only-endpoints")
|
||||
|
||||
@property
|
||||
def replset(self) -> Optional[str]:
|
||||
"""Returns the replicaset name.
|
||||
|
||||
MongoDB only.
|
||||
"""
|
||||
if not self.relation.app:
|
||||
return None
|
||||
|
||||
return self.relation.data[self.relation.app].get("replset")
|
||||
|
||||
@property
|
||||
def tls(self) -> Optional[str]:
|
||||
"""Returns whether TLS is configured."""
|
||||
if not self.relation.app:
|
||||
return None
|
||||
|
||||
return self.relation.data[self.relation.app].get("tls")
|
||||
|
||||
@property
|
||||
def tls_ca(self) -> Optional[str]:
|
||||
"""Returns TLS CA."""
|
||||
if not self.relation.app:
|
||||
return None
|
||||
|
||||
return self.relation.data[self.relation.app].get("tls-ca")
|
||||
|
||||
@property
|
||||
def uris(self) -> Optional[str]:
|
||||
"""Returns the connection URIs.
|
||||
|
||||
MongoDB, Redis, OpenSearch and Kafka only.
|
||||
"""
|
||||
if not self.relation.app:
|
||||
return None
|
||||
|
||||
return self.relation.data[self.relation.app].get("uris")
|
||||
|
||||
@property
|
||||
def username(self) -> Optional[str]:
|
||||
"""Returns the created username."""
|
||||
if not self.relation.app:
|
||||
return None
|
||||
|
||||
return self.relation.data[self.relation.app].get("username")
|
||||
|
||||
@property
|
||||
def version(self) -> Optional[str]:
|
||||
"""Returns the version of the database.
|
||||
|
||||
Version as informed by the database daemon.
|
||||
"""
|
||||
if not self.relation.app:
|
||||
return None
|
||||
|
||||
return self.relation.data[self.relation.app].get("version")
|
||||
|
||||
|
||||
class DatabaseCreatedEvent(DatabaseEvent):
|
||||
"""Event emitted when a new database is created for use on this relation."""
|
||||
|
||||
|
||||
class DatabaseEndpointsChangedEvent(DatabaseEvent):
|
||||
"""Event emitted when the read/write endpoints are changed."""
|
||||
|
||||
|
||||
class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent):
|
||||
"""Event emitted when the read only endpoints are changed."""
|
||||
|
||||
|
||||
class DatabaseEvents(CharmEvents):
|
||||
"""Database events.
|
||||
|
||||
This class defines the events that the database can emit.
|
||||
"""
|
||||
|
||||
database_created = EventSource(DatabaseCreatedEvent)
|
||||
endpoints_changed = EventSource(DatabaseEndpointsChangedEvent)
|
||||
read_only_endpoints_changed = EventSource(DatabaseReadOnlyEndpointsChangedEvent)
|
||||
|
||||
|
||||
Diff = namedtuple("Diff", "added changed deleted")
|
||||
Diff.__doc__ = """
|
||||
A tuple for storing the diff between two data mappings.
|
||||
|
||||
— added — keys that were added.
|
||||
— changed — keys that still exist but have new values.
|
||||
— deleted — keys that were deleted.
|
||||
"""
|
||||
|
||||
|
||||
class DatabaseRequires(Object):
|
||||
"""Requires-side of the database relation."""
|
||||
|
||||
on = DatabaseEvents() # pyright: ignore [reportGeneralTypeIssues]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
charm,
|
||||
relation_name: str,
|
||||
database_name: str,
|
||||
extra_user_roles: Optional[str] = None,
|
||||
relations_aliases: Optional[List[str]] = None,
|
||||
):
|
||||
"""Manager of database client relations."""
|
||||
super().__init__(charm, relation_name)
|
||||
self.charm = charm
|
||||
self.database = database_name
|
||||
self.extra_user_roles = extra_user_roles
|
||||
self.local_app = self.charm.model.app
|
||||
self.local_unit = self.charm.unit
|
||||
self.relation_name = relation_name
|
||||
self.relations_aliases = relations_aliases
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_joined, self._on_relation_joined_event
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_changed, self._on_relation_changed_event
|
||||
)
|
||||
|
||||
# Define custom event names for each alias.
|
||||
if relations_aliases:
|
||||
# Ensure the number of aliases does not exceed the maximum
|
||||
# of connections allowed in the specific relation.
|
||||
relation_connection_limit = self.charm.meta.requires[relation_name].limit
|
||||
if len(relations_aliases) != relation_connection_limit:
|
||||
raise ValueError(
|
||||
f"The number of aliases must match the maximum number of connections allowed in the relation. "
|
||||
f"Expected {relation_connection_limit}, got {len(relations_aliases)}"
|
||||
)
|
||||
|
||||
for relation_alias in relations_aliases:
|
||||
self.on.define_event(f"{relation_alias}_database_created", DatabaseCreatedEvent)
|
||||
self.on.define_event(
|
||||
f"{relation_alias}_endpoints_changed", DatabaseEndpointsChangedEvent
|
||||
)
|
||||
self.on.define_event(
|
||||
f"{relation_alias}_read_only_endpoints_changed",
|
||||
DatabaseReadOnlyEndpointsChangedEvent,
|
||||
)
|
||||
|
||||
def _assign_relation_alias(self, relation_id: int) -> None:
|
||||
"""Assigns an alias to a relation.
|
||||
|
||||
This function writes in the unit data bag.
|
||||
|
||||
Args:
|
||||
relation_id: the identifier for a particular relation.
|
||||
"""
|
||||
# If no aliases were provided, return immediately.
|
||||
if not self.relations_aliases:
|
||||
return
|
||||
|
||||
# Return if an alias was already assigned to this relation
|
||||
# (like when there are more than one unit joining the relation).
|
||||
if (
|
||||
self.charm.model.get_relation(self.relation_name, relation_id)
|
||||
.data[self.local_unit]
|
||||
.get("alias")
|
||||
):
|
||||
return
|
||||
|
||||
# Retrieve the available aliases (the ones that weren't assigned to any relation).
|
||||
available_aliases = self.relations_aliases[:]
|
||||
for relation in self.charm.model.relations[self.relation_name]:
|
||||
alias = relation.data[self.local_unit].get("alias")
|
||||
if alias:
|
||||
logger.debug("Alias %s was already assigned to relation %d", alias, relation.id)
|
||||
available_aliases.remove(alias)
|
||||
|
||||
# Set the alias in the unit relation databag of the specific relation.
|
||||
relation = self.charm.model.get_relation(self.relation_name, relation_id)
|
||||
relation.data[self.local_unit].update({"alias": available_aliases[0]})
|
||||
|
||||
def _diff(self, event: RelationChangedEvent) -> Diff:
|
||||
"""Retrieves the diff of the data in the relation changed databag.
|
||||
|
||||
Args:
|
||||
event: relation changed event.
|
||||
|
||||
Returns:
|
||||
a Diff instance containing the added, deleted and changed
|
||||
keys from the event relation databag.
|
||||
"""
|
||||
# Retrieve the old data from the data key in the local unit relation databag.
|
||||
old_data = json.loads(event.relation.data[self.local_unit].get("data", "{}"))
|
||||
# Retrieve the new data from the event relation databag.
|
||||
new_data = (
|
||||
{key: value for key, value in event.relation.data[event.app].items() if key != "data"}
|
||||
if event.app
|
||||
else {}
|
||||
)
|
||||
|
||||
# These are the keys that were added to the databag and triggered this event.
|
||||
added = new_data.keys() - old_data.keys()
|
||||
# These are the keys that were removed from the databag and triggered this event.
|
||||
deleted = old_data.keys() - new_data.keys()
|
||||
# These are the keys that already existed in the databag,
|
||||
# but had their values changed.
|
||||
changed = {
|
||||
key for key in old_data.keys() & new_data.keys() if old_data[key] != new_data[key]
|
||||
}
|
||||
|
||||
# TODO: evaluate the possibility of losing the diff if some error
|
||||
# happens in the charm before the diff is completely checked (DPE-412).
|
||||
# Convert the new_data to a serializable format and save it for a next diff check.
|
||||
event.relation.data[self.local_unit].update({"data": json.dumps(new_data)})
|
||||
|
||||
# Return the diff with all possible changes.
|
||||
return Diff(added, changed, deleted)
|
||||
|
||||
def _emit_aliased_event(self, event: RelationChangedEvent, event_name: str) -> None:
|
||||
"""Emit an aliased event to a particular relation if it has an alias.
|
||||
|
||||
Args:
|
||||
event: the relation changed event that was received.
|
||||
event_name: the name of the event to emit.
|
||||
"""
|
||||
alias = self._get_relation_alias(event.relation.id)
|
||||
if alias:
|
||||
getattr(self.on, f"{alias}_{event_name}").emit(
|
||||
event.relation, app=event.app, unit=event.unit
|
||||
)
|
||||
|
||||
def _get_relation_alias(self, relation_id: int) -> Optional[str]:
|
||||
"""Returns the relation alias.
|
||||
|
||||
Args:
|
||||
relation_id: the identifier for a particular relation.
|
||||
|
||||
Returns:
|
||||
the relation alias or None if the relation was not found.
|
||||
"""
|
||||
for relation in self.charm.model.relations[self.relation_name]:
|
||||
if relation.id == relation_id:
|
||||
return relation.data[self.local_unit].get("alias")
|
||||
return None
|
||||
|
||||
def fetch_relation_data(self) -> dict:
|
||||
"""Retrieves data from relation.
|
||||
|
||||
This function can be used to retrieve data from a relation
|
||||
in the charm code when outside an event callback.
|
||||
|
||||
Returns:
|
||||
a dict of the values stored in the relation data bag
|
||||
for all relation instances (indexed by the relation ID).
|
||||
"""
|
||||
data = {}
|
||||
for relation in self.relations:
|
||||
data[relation.id] = (
|
||||
{key: value for key, value in relation.data[relation.app].items() if key != "data"}
|
||||
if relation.app
|
||||
else {}
|
||||
)
|
||||
return data
|
||||
|
||||
def _update_relation_data(self, relation_id: int, data: dict) -> None:
|
||||
"""Updates a set of key-value pairs in the relation.
|
||||
|
||||
This function writes in the application data bag, therefore,
|
||||
only the leader unit can call it.
|
||||
|
||||
Args:
|
||||
relation_id: the identifier for a particular relation.
|
||||
data: dict containing the key-value pairs
|
||||
that should be updated in the relation.
|
||||
"""
|
||||
if self.local_unit.is_leader():
|
||||
relation = self.charm.model.get_relation(self.relation_name, relation_id)
|
||||
relation.data[self.local_app].update(data)
|
||||
|
||||
def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None:
|
||||
"""Event emitted when the application joins the database relation."""
|
||||
# If relations aliases were provided, assign one to the relation.
|
||||
self._assign_relation_alias(event.relation.id)
|
||||
|
||||
# Sets both database and extra user roles in the relation
|
||||
# if the roles are provided. Otherwise, sets only the database.
|
||||
if self.extra_user_roles:
|
||||
self._update_relation_data(
|
||||
event.relation.id,
|
||||
{
|
||||
"database": self.database,
|
||||
"extra-user-roles": self.extra_user_roles,
|
||||
},
|
||||
)
|
||||
else:
|
||||
self._update_relation_data(event.relation.id, {"database": self.database})
|
||||
|
||||
def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
|
||||
"""Event emitted when the database relation has changed."""
|
||||
# Check which data has changed to emit customs events.
|
||||
diff = self._diff(event)
|
||||
|
||||
# Check if the database is created
|
||||
# (the database charm shared the credentials).
|
||||
if "username" in diff.added and "password" in diff.added:
|
||||
# Emit the default event (the one without an alias).
|
||||
logger.info("database created at %s", datetime.now())
|
||||
getattr(self.on, "database_created").emit(
|
||||
event.relation, app=event.app, unit=event.unit
|
||||
)
|
||||
|
||||
# Emit the aliased event (if any).
|
||||
self._emit_aliased_event(event, "database_created")
|
||||
|
||||
# To avoid unnecessary application restarts do not trigger
|
||||
# “endpoints_changed“ event if “database_created“ is triggered.
|
||||
return
|
||||
|
||||
# Emit an endpoints changed event if the database
|
||||
# added or changed this info in the relation databag.
|
||||
if "endpoints" in diff.added or "endpoints" in diff.changed:
|
||||
# Emit the default event (the one without an alias).
|
||||
logger.info("endpoints changed on %s", datetime.now())
|
||||
getattr(self.on, "endpoints_changed").emit(
|
||||
event.relation, app=event.app, unit=event.unit
|
||||
)
|
||||
|
||||
# Emit the aliased event (if any).
|
||||
self._emit_aliased_event(event, "endpoints_changed")
|
||||
|
||||
# To avoid unnecessary application restarts do not trigger
|
||||
# “read_only_endpoints_changed“ event if “endpoints_changed“ is triggered.
|
||||
return
|
||||
|
||||
# Emit a read only endpoints changed event if the database
|
||||
# added or changed this info in the relation databag.
|
||||
if "read-only-endpoints" in diff.added or "read-only-endpoints" in diff.changed:
|
||||
# Emit the default event (the one without an alias).
|
||||
logger.info("read-only-endpoints changed on %s", datetime.now())
|
||||
getattr(self.on, "read_only_endpoints_changed").emit(
|
||||
event.relation, app=event.app, unit=event.unit
|
||||
)
|
||||
|
||||
# Emit the aliased event (if any).
|
||||
self._emit_aliased_event(event, "read_only_endpoints_changed")
|
||||
|
||||
@property
|
||||
def relations(self) -> List[Relation]:
|
||||
"""The list of Relation instances associated with this relation_name."""
|
||||
return list(self.charm.model.relations[self.relation_name])
|
@ -1,392 +0,0 @@
|
||||
"""IdentityResourceProvides and Requires module.
|
||||
|
||||
This library contains the Requires and Provides classes for handling
|
||||
the identity_ops interface.
|
||||
|
||||
Import `IdentityResourceRequires` in your charm, with the charm object and the
|
||||
relation name:
|
||||
- self
|
||||
- "identity_ops"
|
||||
|
||||
Also provide additional parameters to the charm object:
|
||||
- request
|
||||
|
||||
Three events are also available to respond to:
|
||||
- provider_ready
|
||||
- provider_goneaway
|
||||
- response_avaialable
|
||||
|
||||
A basic example showing the usage of this relation follows:
|
||||
|
||||
```
|
||||
from charms.keystone_k8s.v0.identity_resource import IdentityResourceRequires
|
||||
|
||||
class IdentityResourceClientCharm(CharmBase):
|
||||
def __init__(self, *args):
|
||||
super().__init__(*args)
|
||||
# IdentityResource Requires
|
||||
self.identity_resource = IdentityResourceRequires(
|
||||
self, "identity_ops",
|
||||
)
|
||||
self.framework.observe(
|
||||
self.identity_resource.on.provider_ready, self._on_identity_resource_ready)
|
||||
self.framework.observe(
|
||||
self.identity_resource.on.provider_goneaway, self._on_identity_resource_goneaway)
|
||||
self.framework.observe(
|
||||
self.identity_resource.on.response_available, self._on_identity_resource_response)
|
||||
|
||||
def _on_identity_resource_ready(self, event):
|
||||
'''React to the IdentityResource provider_ready event.
|
||||
|
||||
This event happens when n IdentityResource relation is added to the
|
||||
model. Ready to send any ops to keystone.
|
||||
'''
|
||||
# Ready to send any ops.
|
||||
pass
|
||||
|
||||
def _on_identity_resource_response(self, event):
|
||||
'''React to the IdentityResource response_available event.
|
||||
|
||||
The IdentityResource interface will provide the response for the ops sent.
|
||||
'''
|
||||
# Read the response for the ops sent.
|
||||
pass
|
||||
|
||||
def _on_identity_resource_goneaway(self, event):
|
||||
'''React to the IdentityResource goneaway event.
|
||||
|
||||
This event happens when an IdentityResource relation is removed.
|
||||
'''
|
||||
# IdentityResource Relation has goneaway. No ops can be sent.
|
||||
pass
|
||||
```
|
||||
|
||||
A sample ops request can be of format
|
||||
{
|
||||
"id": <request id>
|
||||
"tag": <string to identify request>
|
||||
"ops": [
|
||||
{
|
||||
"name": <op name>,
|
||||
"params": {
|
||||
<param 1>: <value 1>,
|
||||
<param 2>: <value 2>
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
For any sensitive data in the ops params, the charm can create secrets and pass
|
||||
secret id instead of sensitive data as part of ops request. The charm should
|
||||
ensure to grant secret access to provider charm i.e., keystone over relation.
|
||||
The secret content should hold the sensitive data with same name as param name.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import (
|
||||
Optional,
|
||||
)
|
||||
|
||||
from ops.charm import (
|
||||
CharmBase,
|
||||
RelationBrokenEvent,
|
||||
RelationChangedEvent,
|
||||
RelationEvent,
|
||||
RelationJoinedEvent,
|
||||
)
|
||||
from ops.framework import (
|
||||
EventBase,
|
||||
EventSource,
|
||||
Object,
|
||||
ObjectEvents,
|
||||
StoredState,
|
||||
)
|
||||
from ops.model import (
|
||||
Relation,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# The unique Charmhub library identifier, never change it
|
||||
LIBID = "b419d4d8249e423487daafc3665ed06f"
|
||||
|
||||
# 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 = 3
|
||||
|
||||
|
||||
REQUEST_NOT_SENT = 1
|
||||
REQUEST_SENT = 2
|
||||
REQUEST_PROCESSED = 3
|
||||
|
||||
|
||||
class IdentityOpsProviderReadyEvent(RelationEvent):
|
||||
"""Has IdentityOpsProviderReady Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class IdentityOpsResponseEvent(RelationEvent):
|
||||
"""Has IdentityOpsResponse Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class IdentityOpsProviderGoneAwayEvent(RelationEvent):
|
||||
"""Has IdentityOpsProviderGoneAway Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class IdentityResourceResponseEvents(ObjectEvents):
|
||||
"""Events class for `on`."""
|
||||
|
||||
provider_ready = EventSource(IdentityOpsProviderReadyEvent)
|
||||
response_available = EventSource(IdentityOpsResponseEvent)
|
||||
provider_goneaway = EventSource(IdentityOpsProviderGoneAwayEvent)
|
||||
|
||||
|
||||
class IdentityResourceRequires(Object):
|
||||
"""IdentityResourceRequires class."""
|
||||
|
||||
on = IdentityResourceResponseEvents()
|
||||
_stored = StoredState()
|
||||
|
||||
def __init__(self, charm: CharmBase, relation_name: str):
|
||||
super().__init__(charm, relation_name)
|
||||
self.charm = charm
|
||||
self.relation_name = relation_name
|
||||
self._stored.set_default(provider_ready=False, requests=[])
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_joined,
|
||||
self._on_identity_resource_relation_joined,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_changed,
|
||||
self._on_identity_resource_relation_changed,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_broken,
|
||||
self._on_identity_resource_relation_broken,
|
||||
)
|
||||
|
||||
def _on_identity_resource_relation_joined(
|
||||
self, event: RelationJoinedEvent
|
||||
):
|
||||
"""Handle IdentityResource joined."""
|
||||
self._stored.provider_ready = True
|
||||
self.on.provider_ready.emit(event.relation)
|
||||
|
||||
def _on_identity_resource_relation_changed(
|
||||
self, event: RelationChangedEvent
|
||||
):
|
||||
"""Handle IdentityResource changed."""
|
||||
id_ = self.response.get("id")
|
||||
self.save_request_in_store(id_, None, None, REQUEST_PROCESSED)
|
||||
self.on.response_available.emit(event.relation)
|
||||
|
||||
def _on_identity_resource_relation_broken(
|
||||
self, event: RelationBrokenEvent
|
||||
):
|
||||
"""Handle IdentityResource broken."""
|
||||
self._stored.provider_ready = False
|
||||
self.on.provider_goneaway.emit(event.relation)
|
||||
|
||||
@property
|
||||
def _identity_resource_rel(self) -> Optional[Relation]:
|
||||
"""The IdentityResource relation."""
|
||||
return self.framework.model.get_relation(self.relation_name)
|
||||
|
||||
@property
|
||||
def response(self) -> dict:
|
||||
"""Response object from keystone."""
|
||||
response = self.get_remote_app_data("response")
|
||||
if not response:
|
||||
return {}
|
||||
|
||||
try:
|
||||
return json.loads(response)
|
||||
except Exception as e:
|
||||
logger.debug(str(e))
|
||||
|
||||
return {}
|
||||
|
||||
def save_request_in_store(
|
||||
self, id: str, tag: str, ops: list, state: int
|
||||
) -> None:
|
||||
"""Save request in the store."""
|
||||
if id is None:
|
||||
return
|
||||
|
||||
for request in self._stored.requests:
|
||||
if request.get("id") == id:
|
||||
if tag:
|
||||
request["tag"] = tag
|
||||
if ops:
|
||||
request["ops"] = ops
|
||||
request["state"] = state
|
||||
return
|
||||
|
||||
# New request
|
||||
self._stored.requests.append(
|
||||
{"id": id, "tag": tag, "ops": ops, "state": state}
|
||||
)
|
||||
|
||||
def get_request_from_store(self, id: str) -> dict:
|
||||
"""Get request from the stote."""
|
||||
for request in self._stored.requests:
|
||||
if request.get("id") == id:
|
||||
return request
|
||||
|
||||
return {}
|
||||
|
||||
def is_request_processed(self, id: str) -> bool:
|
||||
"""Check if request is processed."""
|
||||
for request in self._stored.requests:
|
||||
if (
|
||||
request.get("id") == id
|
||||
and request.get("state") == REQUEST_PROCESSED
|
||||
):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def get_remote_app_data(self, key: str) -> Optional[str]:
|
||||
"""Return the value for the given key from remote app data."""
|
||||
if self._identity_resource_rel:
|
||||
data = self._identity_resource_rel.data[
|
||||
self._identity_resource_rel.app
|
||||
]
|
||||
return data.get(key)
|
||||
|
||||
return None
|
||||
|
||||
def ready(self) -> bool:
|
||||
"""Interface is ready or not.
|
||||
|
||||
Interface is considered ready if the op request is processed
|
||||
and response is sent. In case of non leader unit, just consider
|
||||
the interface is ready.
|
||||
"""
|
||||
if not self.model.unit.is_leader():
|
||||
logger.debug("Not a leader unit, set the interface to ready")
|
||||
return True
|
||||
|
||||
try:
|
||||
app_data = self._identity_resource_rel.data[self.charm.app]
|
||||
if "request" not in app_data:
|
||||
return False
|
||||
|
||||
request = json.loads(app_data["request"])
|
||||
request_id = request.get("id")
|
||||
response_id = self.response.get("id")
|
||||
if request_id == response_id:
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.debug(str(e))
|
||||
|
||||
return False
|
||||
|
||||
def request_ops(self, request: dict) -> None:
|
||||
"""Request keystone ops."""
|
||||
if not self.model.unit.is_leader():
|
||||
logger.debug("Not a leader unit, not sending request")
|
||||
return
|
||||
|
||||
id_ = request.get("id")
|
||||
tag = request.get("tag")
|
||||
ops = request.get("ops")
|
||||
req = self.get_request_from_store(id_)
|
||||
if req and req.get("state") == REQUEST_PROCESSED:
|
||||
logger.debug("Request {id_} already processed")
|
||||
return
|
||||
|
||||
if not self._stored.provider_ready:
|
||||
self.save_request_in_store(id_, tag, ops, REQUEST_NOT_SENT)
|
||||
logger.debug("Keystone not yet ready to take requests")
|
||||
return
|
||||
|
||||
logger.debug("Requesting ops to keystone")
|
||||
app_data = self._identity_resource_rel.data[self.charm.app]
|
||||
app_data["request"] = json.dumps(request)
|
||||
self.save_request_in_store(id_, tag, ops, REQUEST_SENT)
|
||||
|
||||
|
||||
class IdentityOpsRequestEvent(EventBase):
|
||||
"""Has IdentityOpsRequest Event."""
|
||||
|
||||
def __init__(self, handle, relation_id, relation_name, request):
|
||||
"""Initialise event."""
|
||||
super().__init__(handle)
|
||||
self.relation_id = relation_id
|
||||
self.relation_name = relation_name
|
||||
self.request = request
|
||||
|
||||
def snapshot(self):
|
||||
"""Snapshot the event."""
|
||||
return {
|
||||
"relation_id": self.relation_id,
|
||||
"relation_name": self.relation_name,
|
||||
"request": self.request,
|
||||
}
|
||||
|
||||
def restore(self, snapshot):
|
||||
"""Restore the event."""
|
||||
super().restore(snapshot)
|
||||
self.relation_id = snapshot["relation_id"]
|
||||
self.relation_name = snapshot["relation_name"]
|
||||
self.request = snapshot["request"]
|
||||
|
||||
|
||||
class IdentityResourceProviderEvents(ObjectEvents):
|
||||
"""Events class for `on`."""
|
||||
|
||||
process_op = EventSource(IdentityOpsRequestEvent)
|
||||
|
||||
|
||||
class IdentityResourceProvides(Object):
|
||||
"""IdentityResourceProvides class."""
|
||||
|
||||
on = IdentityResourceProviderEvents()
|
||||
|
||||
def __init__(self, charm: CharmBase, relation_name: str):
|
||||
super().__init__(charm, relation_name)
|
||||
self.charm = charm
|
||||
self.relation_name = relation_name
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_changed,
|
||||
self._on_identity_resource_relation_changed,
|
||||
)
|
||||
|
||||
def _on_identity_resource_relation_changed(
|
||||
self, event: RelationChangedEvent
|
||||
):
|
||||
"""Handle IdentityResource changed."""
|
||||
request = event.relation.data[event.relation.app].get("request", {})
|
||||
self.on.process_op.emit(
|
||||
event.relation.id, event.relation.name, request
|
||||
)
|
||||
|
||||
def set_ops_response(
|
||||
self, relation_id: str, relation_name: str, ops_response: dict
|
||||
) -> None:
|
||||
"""Set response to ops request."""
|
||||
if not self.model.unit.is_leader():
|
||||
logger.debug("Not a leader unit, not sending response")
|
||||
return
|
||||
|
||||
logger.debug("Update response from keystone")
|
||||
_identity_resource_rel = self.charm.model.get_relation(
|
||||
relation_name, relation_id
|
||||
)
|
||||
if not _identity_resource_rel:
|
||||
# Relation has disappeared so skip send of data
|
||||
return
|
||||
|
||||
app_data = _identity_resource_rel.data[self.charm.app]
|
||||
app_data["response"] = json.dumps(ops_response)
|
@ -1,525 +0,0 @@
|
||||
"""IdentityServiceProvides and Requires module.
|
||||
|
||||
|
||||
This library contains the Requires and Provides classes for handling
|
||||
the identity_service interface.
|
||||
|
||||
Import `IdentityServiceRequires` in your charm, with the charm object and the
|
||||
relation name:
|
||||
- self
|
||||
- "identity_service"
|
||||
|
||||
Also provide additional parameters to the charm object:
|
||||
- service
|
||||
- internal_url
|
||||
- public_url
|
||||
- admin_url
|
||||
- region
|
||||
- username
|
||||
- vhost
|
||||
|
||||
Two events are also available to respond to:
|
||||
- connected
|
||||
- ready
|
||||
- goneaway
|
||||
|
||||
A basic example showing the usage of this relation follows:
|
||||
|
||||
```
|
||||
from charms.keystone_k8s.v1.identity_service import IdentityServiceRequires
|
||||
|
||||
class IdentityServiceClientCharm(CharmBase):
|
||||
def __init__(self, *args):
|
||||
super().__init__(*args)
|
||||
# IdentityService Requires
|
||||
self.identity_service = IdentityServiceRequires(
|
||||
self, "identity_service",
|
||||
service = "my-service"
|
||||
internal_url = "http://internal-url"
|
||||
public_url = "http://public-url"
|
||||
admin_url = "http://admin-url"
|
||||
region = "region"
|
||||
)
|
||||
self.framework.observe(
|
||||
self.identity_service.on.connected, self._on_identity_service_connected)
|
||||
self.framework.observe(
|
||||
self.identity_service.on.ready, self._on_identity_service_ready)
|
||||
self.framework.observe(
|
||||
self.identity_service.on.goneaway, self._on_identity_service_goneaway)
|
||||
|
||||
def _on_identity_service_connected(self, event):
|
||||
'''React to the IdentityService connected event.
|
||||
|
||||
This event happens when n IdentityService relation is added to the
|
||||
model before credentials etc have been provided.
|
||||
'''
|
||||
# Do something before the relation is complete
|
||||
pass
|
||||
|
||||
def _on_identity_service_ready(self, event):
|
||||
'''React to the IdentityService ready event.
|
||||
|
||||
The IdentityService interface will use the provided config for the
|
||||
request to the identity server.
|
||||
'''
|
||||
# IdentityService Relation is ready. Do something with the completed relation.
|
||||
pass
|
||||
|
||||
def _on_identity_service_goneaway(self, event):
|
||||
'''React to the IdentityService goneaway event.
|
||||
|
||||
This event happens when an IdentityService relation is removed.
|
||||
'''
|
||||
# IdentityService Relation has goneaway. shutdown services or suchlike
|
||||
pass
|
||||
```
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
from ops.framework import (
|
||||
StoredState,
|
||||
EventBase,
|
||||
ObjectEvents,
|
||||
EventSource,
|
||||
Object,
|
||||
)
|
||||
from ops.model import (
|
||||
Relation,
|
||||
SecretNotFoundError,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# The unique Charmhub library identifier, never change it
|
||||
LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0"
|
||||
|
||||
# Increment this major API version when introducing breaking changes
|
||||
LIBAPI = 1
|
||||
|
||||
# Increment this PATCH version before using `charmcraft publish-lib` or reset
|
||||
# to 0 if you are raising the major API version
|
||||
LIBPATCH = 1
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IdentityServiceConnectedEvent(EventBase):
|
||||
"""IdentityService connected Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class IdentityServiceReadyEvent(EventBase):
|
||||
"""IdentityService ready for use Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class IdentityServiceGoneAwayEvent(EventBase):
|
||||
"""IdentityService relation has gone-away Event"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class IdentityServiceServerEvents(ObjectEvents):
|
||||
"""Events class for `on`"""
|
||||
|
||||
connected = EventSource(IdentityServiceConnectedEvent)
|
||||
ready = EventSource(IdentityServiceReadyEvent)
|
||||
goneaway = EventSource(IdentityServiceGoneAwayEvent)
|
||||
|
||||
|
||||
class IdentityServiceRequires(Object):
|
||||
"""
|
||||
IdentityServiceRequires class
|
||||
"""
|
||||
|
||||
on = IdentityServiceServerEvents()
|
||||
_stored = StoredState()
|
||||
|
||||
def __init__(self, charm, relation_name: str, service_endpoints: dict,
|
||||
region: str):
|
||||
super().__init__(charm, relation_name)
|
||||
self.charm = charm
|
||||
self.relation_name = relation_name
|
||||
self.service_endpoints = service_endpoints
|
||||
self.region = region
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_joined,
|
||||
self._on_identity_service_relation_joined,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_changed,
|
||||
self._on_identity_service_relation_changed,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_departed,
|
||||
self._on_identity_service_relation_changed,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_broken,
|
||||
self._on_identity_service_relation_broken,
|
||||
)
|
||||
|
||||
def _on_identity_service_relation_joined(self, event):
|
||||
"""IdentityService relation joined."""
|
||||
logging.debug("IdentityService on_joined")
|
||||
self.on.connected.emit()
|
||||
self.register_services(
|
||||
self.service_endpoints,
|
||||
self.region)
|
||||
|
||||
def _on_identity_service_relation_changed(self, event):
|
||||
"""IdentityService relation changed."""
|
||||
logging.debug("IdentityService on_changed")
|
||||
try:
|
||||
self.service_password
|
||||
self.on.ready.emit()
|
||||
except (AttributeError, KeyError):
|
||||
pass
|
||||
|
||||
def _on_identity_service_relation_broken(self, event):
|
||||
"""IdentityService relation broken."""
|
||||
logging.debug("IdentityService on_broken")
|
||||
self.on.goneaway.emit()
|
||||
|
||||
@property
|
||||
def _identity_service_rel(self) -> Relation:
|
||||
"""The IdentityService relation."""
|
||||
return self.framework.model.get_relation(self.relation_name)
|
||||
|
||||
def get_remote_app_data(self, key: str) -> str:
|
||||
"""Return the value for the given key from remote app data."""
|
||||
data = self._identity_service_rel.data[self._identity_service_rel.app]
|
||||
return data.get(key)
|
||||
|
||||
@property
|
||||
def api_version(self) -> str:
|
||||
"""Return the api_version."""
|
||||
return self.get_remote_app_data('api-version')
|
||||
|
||||
@property
|
||||
def auth_host(self) -> str:
|
||||
"""Return the auth_host."""
|
||||
return self.get_remote_app_data('auth-host')
|
||||
|
||||
@property
|
||||
def auth_port(self) -> str:
|
||||
"""Return the auth_port."""
|
||||
return self.get_remote_app_data('auth-port')
|
||||
|
||||
@property
|
||||
def auth_protocol(self) -> str:
|
||||
"""Return the auth_protocol."""
|
||||
return self.get_remote_app_data('auth-protocol')
|
||||
|
||||
@property
|
||||
def internal_host(self) -> str:
|
||||
"""Return the internal_host."""
|
||||
return self.get_remote_app_data('internal-host')
|
||||
|
||||
@property
|
||||
def internal_port(self) -> str:
|
||||
"""Return the internal_port."""
|
||||
return self.get_remote_app_data('internal-port')
|
||||
|
||||
@property
|
||||
def internal_protocol(self) -> str:
|
||||
"""Return the internal_protocol."""
|
||||
return self.get_remote_app_data('internal-protocol')
|
||||
|
||||
@property
|
||||
def admin_domain_name(self) -> str:
|
||||
"""Return the admin_domain_name."""
|
||||
return self.get_remote_app_data('admin-domain-name')
|
||||
|
||||
@property
|
||||
def admin_domain_id(self) -> str:
|
||||
"""Return the admin_domain_id."""
|
||||
return self.get_remote_app_data('admin-domain-id')
|
||||
|
||||
@property
|
||||
def admin_project_name(self) -> str:
|
||||
"""Return the admin_project_name."""
|
||||
return self.get_remote_app_data('admin-project-name')
|
||||
|
||||
@property
|
||||
def admin_project_id(self) -> str:
|
||||
"""Return the admin_project_id."""
|
||||
return self.get_remote_app_data('admin-project-id')
|
||||
|
||||
@property
|
||||
def admin_user_name(self) -> str:
|
||||
"""Return the admin_user_name."""
|
||||
return self.get_remote_app_data('admin-user-name')
|
||||
|
||||
@property
|
||||
def admin_user_id(self) -> str:
|
||||
"""Return the admin_user_id."""
|
||||
return self.get_remote_app_data('admin-user-id')
|
||||
|
||||
@property
|
||||
def service_domain_name(self) -> str:
|
||||
"""Return the service_domain_name."""
|
||||
return self.get_remote_app_data('service-domain-name')
|
||||
|
||||
@property
|
||||
def service_domain_id(self) -> str:
|
||||
"""Return the service_domain_id."""
|
||||
return self.get_remote_app_data('service-domain-id')
|
||||
|
||||
@property
|
||||
def service_host(self) -> str:
|
||||
"""Return the service_host."""
|
||||
return self.get_remote_app_data('service-host')
|
||||
|
||||
@property
|
||||
def service_credentials(self) -> str:
|
||||
"""Return the service_credentials secret."""
|
||||
return self.get_remote_app_data('service-credentials')
|
||||
|
||||
@property
|
||||
def service_password(self) -> str:
|
||||
"""Return the service_password."""
|
||||
credentials_id = self.get_remote_app_data('service-credentials')
|
||||
if not credentials_id:
|
||||
return None
|
||||
|
||||
try:
|
||||
credentials = self.charm.model.get_secret(id=credentials_id)
|
||||
return credentials.get_content().get("password")
|
||||
except SecretNotFoundError:
|
||||
logger.warning(f"Secret {credentials_id} not found")
|
||||
return None
|
||||
|
||||
@property
|
||||
def service_port(self) -> str:
|
||||
"""Return the service_port."""
|
||||
return self.get_remote_app_data('service-port')
|
||||
|
||||
@property
|
||||
def service_protocol(self) -> str:
|
||||
"""Return the service_protocol."""
|
||||
return self.get_remote_app_data('service-protocol')
|
||||
|
||||
@property
|
||||
def service_project_name(self) -> str:
|
||||
"""Return the service_project_name."""
|
||||
return self.get_remote_app_data('service-project-name')
|
||||
|
||||
@property
|
||||
def service_project_id(self) -> str:
|
||||
"""Return the service_project_id."""
|
||||
return self.get_remote_app_data('service-project-id')
|
||||
|
||||
@property
|
||||
def service_user_name(self) -> str:
|
||||
"""Return the service_user_name."""
|
||||
credentials_id = self.get_remote_app_data('service-credentials')
|
||||
if not credentials_id:
|
||||
return None
|
||||
|
||||
try:
|
||||
credentials = self.charm.model.get_secret(id=credentials_id)
|
||||
return credentials.get_content().get("username")
|
||||
except SecretNotFoundError:
|
||||
logger.warning(f"Secret {credentials_id} not found")
|
||||
return None
|
||||
|
||||
@property
|
||||
def service_user_id(self) -> str:
|
||||
"""Return the service_user_id."""
|
||||
return self.get_remote_app_data('service-user-id')
|
||||
|
||||
@property
|
||||
def internal_auth_url(self) -> str:
|
||||
"""Return the internal_auth_url."""
|
||||
return self.get_remote_app_data('internal-auth-url')
|
||||
|
||||
@property
|
||||
def admin_auth_url(self) -> str:
|
||||
"""Return the admin_auth_url."""
|
||||
return self.get_remote_app_data('admin-auth-url')
|
||||
|
||||
@property
|
||||
def public_auth_url(self) -> str:
|
||||
"""Return the public_auth_url."""
|
||||
return self.get_remote_app_data('public-auth-url')
|
||||
|
||||
@property
|
||||
def admin_role(self) -> str:
|
||||
"""Return the admin_role."""
|
||||
return self.get_remote_app_data('admin-role')
|
||||
|
||||
def register_services(self, service_endpoints: dict,
|
||||
region: str) -> None:
|
||||
"""Request access to the IdentityService server."""
|
||||
if self.model.unit.is_leader():
|
||||
logging.debug("Requesting service registration")
|
||||
app_data = self._identity_service_rel.data[self.charm.app]
|
||||
app_data["service-endpoints"] = json.dumps(
|
||||
service_endpoints, sort_keys=True
|
||||
)
|
||||
app_data["region"] = region
|
||||
|
||||
|
||||
class HasIdentityServiceClientsEvent(EventBase):
|
||||
"""Has IdentityServiceClients Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ReadyIdentityServiceClientsEvent(EventBase):
|
||||
"""IdentityServiceClients Ready Event."""
|
||||
|
||||
def __init__(self, handle, relation_id, relation_name, service_endpoints,
|
||||
region, client_app_name):
|
||||
super().__init__(handle)
|
||||
self.relation_id = relation_id
|
||||
self.relation_name = relation_name
|
||||
self.service_endpoints = service_endpoints
|
||||
self.region = region
|
||||
self.client_app_name = client_app_name
|
||||
|
||||
def snapshot(self):
|
||||
return {
|
||||
"relation_id": self.relation_id,
|
||||
"relation_name": self.relation_name,
|
||||
"service_endpoints": self.service_endpoints,
|
||||
"client_app_name": self.client_app_name,
|
||||
"region": self.region}
|
||||
|
||||
def restore(self, snapshot):
|
||||
super().restore(snapshot)
|
||||
self.relation_id = snapshot["relation_id"]
|
||||
self.relation_name = snapshot["relation_name"]
|
||||
self.service_endpoints = snapshot["service_endpoints"]
|
||||
self.region = snapshot["region"]
|
||||
self.client_app_name = snapshot["client_app_name"]
|
||||
|
||||
|
||||
class IdentityServiceClientEvents(ObjectEvents):
|
||||
"""Events class for `on`"""
|
||||
|
||||
has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent)
|
||||
ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent)
|
||||
|
||||
|
||||
class IdentityServiceProvides(Object):
|
||||
"""
|
||||
IdentityServiceProvides class
|
||||
"""
|
||||
|
||||
on = IdentityServiceClientEvents()
|
||||
_stored = StoredState()
|
||||
|
||||
def __init__(self, charm, relation_name):
|
||||
super().__init__(charm, relation_name)
|
||||
self.charm = charm
|
||||
self.relation_name = relation_name
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_joined,
|
||||
self._on_identity_service_relation_joined,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_changed,
|
||||
self._on_identity_service_relation_changed,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_broken,
|
||||
self._on_identity_service_relation_broken,
|
||||
)
|
||||
|
||||
def _on_identity_service_relation_joined(self, event):
|
||||
"""Handle IdentityService joined."""
|
||||
logging.debug("IdentityService on_joined")
|
||||
self.on.has_identity_service_clients.emit()
|
||||
|
||||
def _on_identity_service_relation_changed(self, event):
|
||||
"""Handle IdentityService changed."""
|
||||
logging.debug("IdentityService on_changed")
|
||||
REQUIRED_KEYS = [
|
||||
'service-endpoints',
|
||||
'region']
|
||||
|
||||
values = [
|
||||
event.relation.data[event.relation.app].get(k)
|
||||
for k in REQUIRED_KEYS
|
||||
]
|
||||
# Validate data on the relation
|
||||
if all(values):
|
||||
service_eps = json.loads(
|
||||
event.relation.data[event.relation.app]['service-endpoints'])
|
||||
self.on.ready_identity_service_clients.emit(
|
||||
event.relation.id,
|
||||
event.relation.name,
|
||||
service_eps,
|
||||
event.relation.data[event.relation.app]['region'],
|
||||
event.relation.app.name)
|
||||
|
||||
def _on_identity_service_relation_broken(self, event):
|
||||
"""Handle IdentityService broken."""
|
||||
logging.debug("IdentityServiceProvides on_departed")
|
||||
# TODO clear data on the relation
|
||||
|
||||
def set_identity_service_credentials(self, relation_name: int,
|
||||
relation_id: str,
|
||||
api_version: str,
|
||||
auth_host: str,
|
||||
auth_port: str,
|
||||
auth_protocol: str,
|
||||
internal_host: str,
|
||||
internal_port: str,
|
||||
internal_protocol: str,
|
||||
service_host: str,
|
||||
service_port: str,
|
||||
service_protocol: str,
|
||||
admin_domain: str,
|
||||
admin_project: str,
|
||||
admin_user: str,
|
||||
service_domain: str,
|
||||
service_project: str,
|
||||
service_user: str,
|
||||
internal_auth_url: str,
|
||||
admin_auth_url: str,
|
||||
public_auth_url: str,
|
||||
service_credentials: str,
|
||||
admin_role: str):
|
||||
logging.debug("Setting identity_service connection information.")
|
||||
_identity_service_rel = None
|
||||
for relation in self.framework.model.relations[relation_name]:
|
||||
if relation.id == relation_id:
|
||||
_identity_service_rel = relation
|
||||
if not _identity_service_rel:
|
||||
# Relation has disappeared so skip send of data
|
||||
return
|
||||
app_data = _identity_service_rel.data[self.charm.app]
|
||||
app_data["api-version"] = api_version
|
||||
app_data["auth-host"] = auth_host
|
||||
app_data["auth-port"] = str(auth_port)
|
||||
app_data["auth-protocol"] = auth_protocol
|
||||
app_data["internal-host"] = internal_host
|
||||
app_data["internal-port"] = str(internal_port)
|
||||
app_data["internal-protocol"] = internal_protocol
|
||||
app_data["service-host"] = service_host
|
||||
app_data["service-port"] = str(service_port)
|
||||
app_data["service-protocol"] = service_protocol
|
||||
app_data["admin-domain-name"] = admin_domain.name
|
||||
app_data["admin-domain-id"] = admin_domain.id
|
||||
app_data["admin-project-name"] = admin_project.name
|
||||
app_data["admin-project-id"] = admin_project.id
|
||||
app_data["admin-user-name"] = admin_user.name
|
||||
app_data["admin-user-id"] = admin_user.id
|
||||
app_data["service-domain-name"] = service_domain.name
|
||||
app_data["service-domain-id"] = service_domain.id
|
||||
app_data["service-project-name"] = service_project.name
|
||||
app_data["service-project-id"] = service_project.id
|
||||
app_data["service-user-id"] = service_user.id
|
||||
app_data["internal-auth-url"] = internal_auth_url
|
||||
app_data["admin-auth-url"] = admin_auth_url
|
||||
app_data["public-auth-url"] = public_auth_url
|
||||
app_data["service-credentials"] = service_credentials
|
||||
app_data["admin-role"] = admin_role
|
@ -1,286 +0,0 @@
|
||||
"""RabbitMQProvides and Requires module.
|
||||
|
||||
This library contains the Requires and Provides classes for handling
|
||||
the rabbitmq interface.
|
||||
|
||||
Import `RabbitMQRequires` in your charm, with the charm object and the
|
||||
relation name:
|
||||
- self
|
||||
- "amqp"
|
||||
|
||||
Also provide two additional parameters to the charm object:
|
||||
- username
|
||||
- vhost
|
||||
|
||||
Two events are also available to respond to:
|
||||
- connected
|
||||
- ready
|
||||
- goneaway
|
||||
|
||||
A basic example showing the usage of this relation follows:
|
||||
|
||||
```
|
||||
from charms.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires
|
||||
|
||||
class RabbitMQClientCharm(CharmBase):
|
||||
def __init__(self, *args):
|
||||
super().__init__(*args)
|
||||
# RabbitMQ Requires
|
||||
self.amqp = RabbitMQRequires(
|
||||
self, "amqp",
|
||||
username="myusername",
|
||||
vhost="vhostname"
|
||||
)
|
||||
self.framework.observe(
|
||||
self.amqp.on.connected, self._on_amqp_connected)
|
||||
self.framework.observe(
|
||||
self.amqp.on.ready, self._on_amqp_ready)
|
||||
self.framework.observe(
|
||||
self.amqp.on.goneaway, self._on_amqp_goneaway)
|
||||
|
||||
def _on_amqp_connected(self, event):
|
||||
'''React to the RabbitMQ connected event.
|
||||
|
||||
This event happens when n RabbitMQ relation is added to the
|
||||
model before credentials etc have been provided.
|
||||
'''
|
||||
# Do something before the relation is complete
|
||||
pass
|
||||
|
||||
def _on_amqp_ready(self, event):
|
||||
'''React to the RabbitMQ ready event.
|
||||
|
||||
The RabbitMQ interface will use the provided username and vhost for the
|
||||
request to the rabbitmq server.
|
||||
'''
|
||||
# RabbitMQ Relation is ready. Do something with the completed relation.
|
||||
pass
|
||||
|
||||
def _on_amqp_goneaway(self, event):
|
||||
'''React to the RabbitMQ goneaway event.
|
||||
|
||||
This event happens when an RabbitMQ relation is removed.
|
||||
'''
|
||||
# RabbitMQ Relation has goneaway. shutdown services or suchlike
|
||||
pass
|
||||
```
|
||||
"""
|
||||
|
||||
# The unique Charmhub library identifier, never change it
|
||||
LIBID = "45622352791142fd9cf87232e3bd6f2a"
|
||||
|
||||
# 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
|
||||
|
||||
import logging
|
||||
|
||||
from ops.framework import (
|
||||
StoredState,
|
||||
EventBase,
|
||||
ObjectEvents,
|
||||
EventSource,
|
||||
Object,
|
||||
)
|
||||
|
||||
from ops.model import Relation
|
||||
|
||||
from typing import List
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RabbitMQConnectedEvent(EventBase):
|
||||
"""RabbitMQ connected Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class RabbitMQReadyEvent(EventBase):
|
||||
"""RabbitMQ ready for use Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class RabbitMQGoneAwayEvent(EventBase):
|
||||
"""RabbitMQ relation has gone-away Event"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class RabbitMQServerEvents(ObjectEvents):
|
||||
"""Events class for `on`"""
|
||||
|
||||
connected = EventSource(RabbitMQConnectedEvent)
|
||||
ready = EventSource(RabbitMQReadyEvent)
|
||||
goneaway = EventSource(RabbitMQGoneAwayEvent)
|
||||
|
||||
|
||||
class RabbitMQRequires(Object):
|
||||
"""
|
||||
RabbitMQRequires class
|
||||
"""
|
||||
|
||||
on = RabbitMQServerEvents()
|
||||
|
||||
def __init__(self, charm, relation_name: str, username: str, vhost: str):
|
||||
super().__init__(charm, relation_name)
|
||||
self.charm = charm
|
||||
self.relation_name = relation_name
|
||||
self.username = username
|
||||
self.vhost = vhost
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_joined,
|
||||
self._on_amqp_relation_joined,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_changed,
|
||||
self._on_amqp_relation_changed,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_departed,
|
||||
self._on_amqp_relation_changed,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_broken,
|
||||
self._on_amqp_relation_broken,
|
||||
)
|
||||
|
||||
def _on_amqp_relation_joined(self, event):
|
||||
"""RabbitMQ relation joined."""
|
||||
logging.debug("RabbitMQRabbitMQRequires on_joined")
|
||||
self.on.connected.emit()
|
||||
self.request_access(self.username, self.vhost)
|
||||
|
||||
def _on_amqp_relation_changed(self, event):
|
||||
"""RabbitMQ relation changed."""
|
||||
logging.debug("RabbitMQRabbitMQRequires on_changed/departed")
|
||||
if self.password:
|
||||
self.on.ready.emit()
|
||||
|
||||
def _on_amqp_relation_broken(self, event):
|
||||
"""RabbitMQ relation broken."""
|
||||
logging.debug("RabbitMQRabbitMQRequires on_broken")
|
||||
self.on.goneaway.emit()
|
||||
|
||||
@property
|
||||
def _amqp_rel(self) -> Relation:
|
||||
"""The RabbitMQ relation."""
|
||||
return self.framework.model.get_relation(self.relation_name)
|
||||
|
||||
@property
|
||||
def password(self) -> str:
|
||||
"""Return the RabbitMQ password from the server side of the relation."""
|
||||
return self._amqp_rel.data[self._amqp_rel.app].get("password")
|
||||
|
||||
@property
|
||||
def hostname(self) -> str:
|
||||
"""Return the hostname from the RabbitMQ relation"""
|
||||
return self._amqp_rel.data[self._amqp_rel.app].get("hostname")
|
||||
|
||||
@property
|
||||
def ssl_port(self) -> str:
|
||||
"""Return the SSL port from the RabbitMQ relation"""
|
||||
return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port")
|
||||
|
||||
@property
|
||||
def ssl_ca(self) -> str:
|
||||
"""Return the SSL port from the RabbitMQ relation"""
|
||||
return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca")
|
||||
|
||||
@property
|
||||
def hostnames(self) -> List[str]:
|
||||
"""Return a list of remote RMQ hosts from the RabbitMQ relation"""
|
||||
_hosts = []
|
||||
for unit in self._amqp_rel.units:
|
||||
_hosts.append(self._amqp_rel.data[unit].get("ingress-address"))
|
||||
return _hosts
|
||||
|
||||
def request_access(self, username: str, vhost: str) -> None:
|
||||
"""Request access to the RabbitMQ server."""
|
||||
if self.model.unit.is_leader():
|
||||
logging.debug("Requesting RabbitMQ user and vhost")
|
||||
self._amqp_rel.data[self.charm.app]["username"] = username
|
||||
self._amqp_rel.data[self.charm.app]["vhost"] = vhost
|
||||
|
||||
|
||||
class HasRabbitMQClientsEvent(EventBase):
|
||||
"""Has RabbitMQClients Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ReadyRabbitMQClientsEvent(EventBase):
|
||||
"""RabbitMQClients Ready Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class RabbitMQClientEvents(ObjectEvents):
|
||||
"""Events class for `on`"""
|
||||
|
||||
has_amqp_clients = EventSource(HasRabbitMQClientsEvent)
|
||||
ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent)
|
||||
|
||||
|
||||
class RabbitMQProvides(Object):
|
||||
"""
|
||||
RabbitMQProvides class
|
||||
"""
|
||||
|
||||
on = RabbitMQClientEvents()
|
||||
|
||||
def __init__(self, charm, relation_name, callback):
|
||||
super().__init__(charm, relation_name)
|
||||
self.charm = charm
|
||||
self.relation_name = relation_name
|
||||
self.callback = callback
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_joined,
|
||||
self._on_amqp_relation_joined,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_changed,
|
||||
self._on_amqp_relation_changed,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_broken,
|
||||
self._on_amqp_relation_broken,
|
||||
)
|
||||
|
||||
def _on_amqp_relation_joined(self, event):
|
||||
"""Handle RabbitMQ joined."""
|
||||
logging.debug("RabbitMQRabbitMQProvides on_joined data={}"
|
||||
.format(event.relation.data[event.relation.app]))
|
||||
self.on.has_amqp_clients.emit()
|
||||
|
||||
def _on_amqp_relation_changed(self, event):
|
||||
"""Handle RabbitMQ changed."""
|
||||
logging.debug("RabbitMQRabbitMQProvides on_changed data={}"
|
||||
.format(event.relation.data[event.relation.app]))
|
||||
# Validate data on the relation
|
||||
if self.username(event) and self.vhost(event):
|
||||
self.on.ready_amqp_clients.emit()
|
||||
if self.charm.unit.is_leader():
|
||||
self.callback(event, self.username(event), self.vhost(event))
|
||||
else:
|
||||
logging.warning("Received RabbitMQ changed event without the "
|
||||
"expected keys ('username', 'vhost') in the "
|
||||
"application data bag. Incompatible charm in "
|
||||
"other end of relation?")
|
||||
|
||||
def _on_amqp_relation_broken(self, event):
|
||||
"""Handle RabbitMQ broken."""
|
||||
logging.debug("RabbitMQRabbitMQProvides on_departed")
|
||||
# TODO clear data on the relation
|
||||
|
||||
def username(self, event):
|
||||
"""Return the RabbitMQ username from the client side of the relation."""
|
||||
return event.relation.data[event.relation.app].get("username")
|
||||
|
||||
def vhost(self, event):
|
||||
"""Return the RabbitMQ vhost from the client side of the relation."""
|
||||
return event.relation.data[event.relation.app].get("vhost")
|
@ -1,734 +0,0 @@
|
||||
# Copyright 2023 Canonical Ltd.
|
||||
# See LICENSE file for licensing details.
|
||||
|
||||
r"""# Interface Library for ingress.
|
||||
|
||||
This library wraps relation endpoints using the `ingress` interface
|
||||
and provides a Python API for both requesting and providing per-application
|
||||
ingress, with load-balancing occurring across all units.
|
||||
|
||||
## Getting Started
|
||||
|
||||
To get started using the library, you just need to fetch the library using `charmcraft`.
|
||||
|
||||
```shell
|
||||
cd some-charm
|
||||
charmcraft fetch-lib charms.traefik_k8s.v1.ingress
|
||||
```
|
||||
|
||||
In the `metadata.yaml` of the charm, add the following:
|
||||
|
||||
```yaml
|
||||
requires:
|
||||
ingress:
|
||||
interface: ingress
|
||||
limit: 1
|
||||
```
|
||||
|
||||
Then, to initialise the library:
|
||||
|
||||
```python
|
||||
from charms.traefik_k8s.v2.ingress import (IngressPerAppRequirer,
|
||||
IngressPerAppReadyEvent, IngressPerAppRevokedEvent)
|
||||
|
||||
class SomeCharm(CharmBase):
|
||||
def __init__(self, *args):
|
||||
# ...
|
||||
self.ingress = IngressPerAppRequirer(self, port=80)
|
||||
# The following event is triggered when the ingress URL to be used
|
||||
# by this deployment of the `SomeCharm` is ready (or changes).
|
||||
self.framework.observe(
|
||||
self.ingress.on.ready, self._on_ingress_ready
|
||||
)
|
||||
self.framework.observe(
|
||||
self.ingress.on.revoked, self._on_ingress_revoked
|
||||
)
|
||||
|
||||
def _on_ingress_ready(self, event: IngressPerAppReadyEvent):
|
||||
logger.info("This app's ingress URL: %s", event.url)
|
||||
|
||||
def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent):
|
||||
logger.info("This app no longer has ingress")
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import socket
|
||||
import typing
|
||||
from dataclasses import dataclass
|
||||
from typing import (
|
||||
Any,
|
||||
Dict,
|
||||
List,
|
||||
MutableMapping,
|
||||
Optional,
|
||||
Sequence,
|
||||
Tuple,
|
||||
)
|
||||
|
||||
import pydantic
|
||||
from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent
|
||||
from ops.framework import EventSource, Object, ObjectEvents, StoredState
|
||||
from ops.model import ModelError, Relation, Unit
|
||||
from pydantic import AnyHttpUrl, BaseModel, Field, validator
|
||||
|
||||
# The unique Charmhub library identifier, never change it
|
||||
LIBID = "e6de2a5cd5b34422a204668f3b8f90d2"
|
||||
|
||||
# Increment this major API version when introducing breaking changes
|
||||
LIBAPI = 2
|
||||
|
||||
# Increment this PATCH version before using `charmcraft publish-lib` or reset
|
||||
# to 0 if you are raising the major API version
|
||||
LIBPATCH = 6
|
||||
|
||||
PYDEPS = ["pydantic<2.0"]
|
||||
|
||||
DEFAULT_RELATION_NAME = "ingress"
|
||||
RELATION_INTERFACE = "ingress"
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"}
|
||||
|
||||
|
||||
class DatabagModel(BaseModel):
|
||||
"""Base databag model."""
|
||||
|
||||
class Config:
|
||||
"""Pydantic config."""
|
||||
|
||||
allow_population_by_field_name = True
|
||||
"""Allow instantiating this class by field name (instead of forcing alias)."""
|
||||
|
||||
_NEST_UNDER = None
|
||||
|
||||
@classmethod
|
||||
def load(cls, databag: MutableMapping):
|
||||
"""Load this model from a Juju databag."""
|
||||
if cls._NEST_UNDER:
|
||||
return cls.parse_obj(json.loads(databag[cls._NEST_UNDER]))
|
||||
|
||||
try:
|
||||
data = {k: json.loads(v) for k, v in databag.items() if k not in BUILTIN_JUJU_KEYS}
|
||||
except json.JSONDecodeError as e:
|
||||
msg = f"invalid databag contents: expecting json. {databag}"
|
||||
log.error(msg)
|
||||
raise DataValidationError(msg) from e
|
||||
|
||||
try:
|
||||
return cls.parse_raw(json.dumps(data)) # type: ignore
|
||||
except pydantic.ValidationError as e:
|
||||
msg = f"failed to validate databag: {databag}"
|
||||
log.error(msg, exc_info=True)
|
||||
raise DataValidationError(msg) from e
|
||||
|
||||
def dump(self, databag: Optional[MutableMapping] = None, clear: bool = True):
|
||||
"""Write the contents of this model to Juju databag.
|
||||
|
||||
:param databag: the databag to write the data to.
|
||||
:param clear: ensure the databag is cleared before writing it.
|
||||
"""
|
||||
if clear and databag:
|
||||
databag.clear()
|
||||
|
||||
if databag is None:
|
||||
databag = {}
|
||||
|
||||
if self._NEST_UNDER:
|
||||
databag[self._NEST_UNDER] = self.json()
|
||||
|
||||
dct = self.dict()
|
||||
for key, field in self.__fields__.items(): # type: ignore
|
||||
value = dct[key]
|
||||
databag[field.alias or key] = json.dumps(value)
|
||||
|
||||
return databag
|
||||
|
||||
|
||||
# todo: import these models from charm-relation-interfaces/ingress/v2 instead of redeclaring them
|
||||
class IngressUrl(BaseModel):
|
||||
"""Ingress url schema."""
|
||||
|
||||
url: AnyHttpUrl
|
||||
|
||||
|
||||
class IngressProviderAppData(DatabagModel):
|
||||
"""Ingress application databag schema."""
|
||||
|
||||
ingress: IngressUrl
|
||||
|
||||
|
||||
class ProviderSchema(BaseModel):
|
||||
"""Provider schema for Ingress."""
|
||||
|
||||
app: IngressProviderAppData
|
||||
|
||||
|
||||
class IngressRequirerAppData(DatabagModel):
|
||||
"""Ingress requirer application databag model."""
|
||||
|
||||
model: str = Field(description="The model the application is in.")
|
||||
name: str = Field(description="the name of the app requesting ingress.")
|
||||
port: int = Field(description="The port the app wishes to be exposed.")
|
||||
|
||||
# fields on top of vanilla 'ingress' interface:
|
||||
strip_prefix: Optional[bool] = Field(
|
||||
description="Whether to strip the prefix from the ingress url.", alias="strip-prefix"
|
||||
)
|
||||
redirect_https: Optional[bool] = Field(
|
||||
description="Whether to redirect http traffic to https.", alias="redirect-https"
|
||||
)
|
||||
|
||||
scheme: Optional[str] = Field(
|
||||
default="http", description="What scheme to use in the generated ingress url"
|
||||
)
|
||||
|
||||
@validator("scheme", pre=True)
|
||||
def validate_scheme(cls, scheme): # noqa: N805 # pydantic wants 'cls' as first arg
|
||||
"""Validate scheme arg."""
|
||||
if scheme not in {"http", "https", "h2c"}:
|
||||
raise ValueError("invalid scheme: should be one of `http|https|h2c`")
|
||||
return scheme
|
||||
|
||||
@validator("port", pre=True)
|
||||
def validate_port(cls, port): # noqa: N805 # pydantic wants 'cls' as first arg
|
||||
"""Validate port."""
|
||||
assert isinstance(port, int), type(port)
|
||||
assert 0 < port < 65535, "port out of TCP range"
|
||||
return port
|
||||
|
||||
|
||||
class IngressRequirerUnitData(DatabagModel):
|
||||
"""Ingress requirer unit databag model."""
|
||||
|
||||
host: str = Field(description="Hostname the unit wishes to be exposed.")
|
||||
|
||||
@validator("host", pre=True)
|
||||
def validate_host(cls, host): # noqa: N805 # pydantic wants 'cls' as first arg
|
||||
"""Validate host."""
|
||||
assert isinstance(host, str), type(host)
|
||||
return host
|
||||
|
||||
|
||||
class RequirerSchema(BaseModel):
|
||||
"""Requirer schema for Ingress."""
|
||||
|
||||
app: IngressRequirerAppData
|
||||
unit: IngressRequirerUnitData
|
||||
|
||||
|
||||
class IngressError(RuntimeError):
|
||||
"""Base class for custom errors raised by this library."""
|
||||
|
||||
|
||||
class NotReadyError(IngressError):
|
||||
"""Raised when a relation is not ready."""
|
||||
|
||||
|
||||
class DataValidationError(IngressError):
|
||||
"""Raised when data validation fails on IPU relation data."""
|
||||
|
||||
|
||||
class _IngressPerAppBase(Object):
|
||||
"""Base class for IngressPerUnit interface classes."""
|
||||
|
||||
def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME):
|
||||
super().__init__(charm, relation_name)
|
||||
|
||||
self.charm: CharmBase = charm
|
||||
self.relation_name = relation_name
|
||||
self.app = self.charm.app
|
||||
self.unit = self.charm.unit
|
||||
|
||||
observe = self.framework.observe
|
||||
rel_events = charm.on[relation_name]
|
||||
observe(rel_events.relation_created, self._handle_relation)
|
||||
observe(rel_events.relation_joined, self._handle_relation)
|
||||
observe(rel_events.relation_changed, self._handle_relation)
|
||||
observe(rel_events.relation_broken, self._handle_relation_broken)
|
||||
observe(charm.on.leader_elected, self._handle_upgrade_or_leader) # type: ignore
|
||||
observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader) # type: ignore
|
||||
|
||||
@property
|
||||
def relations(self):
|
||||
"""The list of Relation instances associated with this endpoint."""
|
||||
return list(self.charm.model.relations[self.relation_name])
|
||||
|
||||
def _handle_relation(self, event):
|
||||
"""Subclasses should implement this method to handle a relation update."""
|
||||
pass
|
||||
|
||||
def _handle_relation_broken(self, event):
|
||||
"""Subclasses should implement this method to handle a relation breaking."""
|
||||
pass
|
||||
|
||||
def _handle_upgrade_or_leader(self, event):
|
||||
"""Subclasses should implement this method to handle upgrades or leadership change."""
|
||||
pass
|
||||
|
||||
|
||||
class _IPAEvent(RelationEvent):
|
||||
__args__: Tuple[str, ...] = ()
|
||||
__optional_kwargs__: Dict[str, Any] = {}
|
||||
|
||||
@classmethod
|
||||
def __attrs__(cls):
|
||||
return cls.__args__ + tuple(cls.__optional_kwargs__.keys())
|
||||
|
||||
def __init__(self, handle, relation, *args, **kwargs):
|
||||
super().__init__(handle, relation)
|
||||
|
||||
if not len(self.__args__) == len(args):
|
||||
raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args)))
|
||||
|
||||
for attr, obj in zip(self.__args__, args):
|
||||
setattr(self, attr, obj)
|
||||
for attr, default in self.__optional_kwargs__.items():
|
||||
obj = kwargs.get(attr, default)
|
||||
setattr(self, attr, obj)
|
||||
|
||||
def snapshot(self):
|
||||
dct = super().snapshot()
|
||||
for attr in self.__attrs__():
|
||||
obj = getattr(self, attr)
|
||||
try:
|
||||
dct[attr] = obj
|
||||
except ValueError as e:
|
||||
raise ValueError(
|
||||
"cannot automagically serialize {}: "
|
||||
"override this method and do it "
|
||||
"manually.".format(obj)
|
||||
) from e
|
||||
|
||||
return dct
|
||||
|
||||
def restore(self, snapshot) -> None:
|
||||
super().restore(snapshot)
|
||||
for attr, obj in snapshot.items():
|
||||
setattr(self, attr, obj)
|
||||
|
||||
|
||||
class IngressPerAppDataProvidedEvent(_IPAEvent):
|
||||
"""Event representing that ingress data has been provided for an app."""
|
||||
|
||||
__args__ = ("name", "model", "hosts", "strip_prefix", "redirect_https")
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
name: Optional[str] = None
|
||||
model: Optional[str] = None
|
||||
# sequence of hostname, port dicts
|
||||
hosts: Sequence["IngressRequirerUnitData"] = ()
|
||||
strip_prefix: bool = False
|
||||
redirect_https: bool = False
|
||||
|
||||
|
||||
class IngressPerAppDataRemovedEvent(RelationEvent):
|
||||
"""Event representing that ingress data has been removed for an app."""
|
||||
|
||||
|
||||
class IngressPerAppProviderEvents(ObjectEvents):
|
||||
"""Container for IPA Provider events."""
|
||||
|
||||
data_provided = EventSource(IngressPerAppDataProvidedEvent)
|
||||
data_removed = EventSource(IngressPerAppDataRemovedEvent)
|
||||
|
||||
|
||||
@dataclass
|
||||
class IngressRequirerData:
|
||||
"""Data exposed by the ingress requirer to the provider."""
|
||||
|
||||
app: "IngressRequirerAppData"
|
||||
units: List["IngressRequirerUnitData"]
|
||||
|
||||
|
||||
class TlsProviderType(typing.Protocol):
|
||||
"""Placeholder."""
|
||||
|
||||
@property
|
||||
def enabled(self) -> bool: # type: ignore
|
||||
"""Placeholder."""
|
||||
|
||||
|
||||
class IngressPerAppProvider(_IngressPerAppBase):
|
||||
"""Implementation of the provider of ingress."""
|
||||
|
||||
on = IngressPerAppProviderEvents() # type: ignore
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
charm: CharmBase,
|
||||
relation_name: str = DEFAULT_RELATION_NAME,
|
||||
):
|
||||
"""Constructor for IngressPerAppProvider.
|
||||
|
||||
Args:
|
||||
charm: The charm that is instantiating the instance.
|
||||
relation_name: The name of the relation endpoint to bind to
|
||||
(defaults to "ingress").
|
||||
"""
|
||||
super().__init__(charm, relation_name)
|
||||
|
||||
def _handle_relation(self, event):
|
||||
# created, joined or changed: if remote side has sent the required data:
|
||||
# notify listeners.
|
||||
if self.is_ready(event.relation):
|
||||
data = self.get_data(event.relation)
|
||||
self.on.data_provided.emit( # type: ignore
|
||||
event.relation,
|
||||
data.app.name,
|
||||
data.app.model,
|
||||
[unit.dict() for unit in data.units],
|
||||
data.app.strip_prefix or False,
|
||||
data.app.redirect_https or False,
|
||||
)
|
||||
|
||||
def _handle_relation_broken(self, event):
|
||||
self.on.data_removed.emit(event.relation) # type: ignore
|
||||
|
||||
def wipe_ingress_data(self, relation: Relation):
|
||||
"""Clear ingress data from relation."""
|
||||
assert self.unit.is_leader(), "only leaders can do this"
|
||||
try:
|
||||
relation.data
|
||||
except ModelError as e:
|
||||
log.warning(
|
||||
"error {} accessing relation data for {!r}. "
|
||||
"Probably a ghost of a dead relation is still "
|
||||
"lingering around.".format(e, relation.name)
|
||||
)
|
||||
return
|
||||
del relation.data[self.app]["ingress"]
|
||||
|
||||
def _get_requirer_units_data(self, relation: Relation) -> List["IngressRequirerUnitData"]:
|
||||
"""Fetch and validate the requirer's app databag."""
|
||||
out: List["IngressRequirerUnitData"] = []
|
||||
|
||||
unit: Unit
|
||||
for unit in relation.units:
|
||||
databag = relation.data[unit]
|
||||
try:
|
||||
data = IngressRequirerUnitData.load(databag)
|
||||
out.append(data)
|
||||
except pydantic.ValidationError:
|
||||
log.info(f"failed to validate remote unit data for {unit}")
|
||||
raise
|
||||
return out
|
||||
|
||||
@staticmethod
|
||||
def _get_requirer_app_data(relation: Relation) -> "IngressRequirerAppData":
|
||||
"""Fetch and validate the requirer's app databag."""
|
||||
app = relation.app
|
||||
if app is None:
|
||||
raise NotReadyError(relation)
|
||||
|
||||
databag = relation.data[app]
|
||||
return IngressRequirerAppData.load(databag)
|
||||
|
||||
def get_data(self, relation: Relation) -> IngressRequirerData:
|
||||
"""Fetch the remote (requirer) app and units' databags."""
|
||||
try:
|
||||
return IngressRequirerData(
|
||||
self._get_requirer_app_data(relation), self._get_requirer_units_data(relation)
|
||||
)
|
||||
except (pydantic.ValidationError, DataValidationError) as e:
|
||||
raise DataValidationError("failed to validate ingress requirer data") from e
|
||||
|
||||
def is_ready(self, relation: Optional[Relation] = None):
|
||||
"""The Provider is ready if the requirer has sent valid data."""
|
||||
if not relation:
|
||||
return any(map(self.is_ready, self.relations))
|
||||
|
||||
try:
|
||||
self.get_data(relation)
|
||||
except (DataValidationError, NotReadyError) as e:
|
||||
log.debug("Provider not ready; validation error encountered: %s" % str(e))
|
||||
return False
|
||||
return True
|
||||
|
||||
def _published_url(self, relation: Relation) -> Optional["IngressProviderAppData"]:
|
||||
"""Fetch and validate this app databag; return the ingress url."""
|
||||
if not self.is_ready(relation) or not self.unit.is_leader():
|
||||
# Handle edge case where remote app name can be missing, e.g.,
|
||||
# relation_broken events.
|
||||
# Also, only leader units can read own app databags.
|
||||
# FIXME https://github.com/canonical/traefik-k8s-operator/issues/34
|
||||
return None
|
||||
|
||||
# fetch the provider's app databag
|
||||
databag = relation.data[self.app]
|
||||
if not databag.get("ingress"):
|
||||
raise NotReadyError("This application did not `publish_url` yet.")
|
||||
|
||||
return IngressProviderAppData.load(databag)
|
||||
|
||||
def publish_url(self, relation: Relation, url: str):
|
||||
"""Publish to the app databag the ingress url."""
|
||||
ingress_url = {"url": url}
|
||||
IngressProviderAppData.parse_obj({"ingress": ingress_url}).dump(relation.data[self.app])
|
||||
|
||||
@property
|
||||
def proxied_endpoints(self) -> Dict[str, str]:
|
||||
"""Returns the ingress settings provided to applications by this IngressPerAppProvider.
|
||||
|
||||
For example, when this IngressPerAppProvider has provided the
|
||||
`http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary
|
||||
will be:
|
||||
|
||||
```
|
||||
{
|
||||
"my-app": {
|
||||
"url": "http://foo.bar/my-model.my-app"
|
||||
}
|
||||
}
|
||||
```
|
||||
"""
|
||||
results = {}
|
||||
|
||||
for ingress_relation in self.relations:
|
||||
if not ingress_relation.app:
|
||||
log.warning(
|
||||
f"no app in relation {ingress_relation} when fetching proxied endpoints: skipping"
|
||||
)
|
||||
continue
|
||||
try:
|
||||
ingress_data = self._published_url(ingress_relation)
|
||||
except NotReadyError:
|
||||
log.warning(
|
||||
f"no published url found in {ingress_relation}: "
|
||||
f"traefik didn't publish_url yet to this relation."
|
||||
)
|
||||
continue
|
||||
|
||||
if not ingress_data:
|
||||
log.warning(f"relation {ingress_relation} not ready yet: try again in some time.")
|
||||
continue
|
||||
|
||||
results[ingress_relation.app.name] = ingress_data.ingress.dict()
|
||||
return results
|
||||
|
||||
|
||||
class IngressPerAppReadyEvent(_IPAEvent):
|
||||
"""Event representing that ingress for an app is ready."""
|
||||
|
||||
__args__ = ("url",)
|
||||
if typing.TYPE_CHECKING:
|
||||
url: Optional[str] = None
|
||||
|
||||
|
||||
class IngressPerAppRevokedEvent(RelationEvent):
|
||||
"""Event representing that ingress for an app has been revoked."""
|
||||
|
||||
|
||||
class IngressPerAppRequirerEvents(ObjectEvents):
|
||||
"""Container for IPA Requirer events."""
|
||||
|
||||
ready = EventSource(IngressPerAppReadyEvent)
|
||||
revoked = EventSource(IngressPerAppRevokedEvent)
|
||||
|
||||
|
||||
class IngressPerAppRequirer(_IngressPerAppBase):
|
||||
"""Implementation of the requirer of the ingress relation."""
|
||||
|
||||
on = IngressPerAppRequirerEvents() # type: ignore
|
||||
|
||||
# used to prevent spurious urls to be sent out if the event we're currently
|
||||
# handling is a relation-broken one.
|
||||
_stored = StoredState()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
charm: CharmBase,
|
||||
relation_name: str = DEFAULT_RELATION_NAME,
|
||||
*,
|
||||
host: Optional[str] = None,
|
||||
port: Optional[int] = None,
|
||||
strip_prefix: bool = False,
|
||||
redirect_https: bool = False,
|
||||
# fixme: this is horrible UX.
|
||||
# shall we switch to manually calling provide_ingress_requirements with all args when ready?
|
||||
scheme: typing.Callable[[], str] = lambda: "http",
|
||||
):
|
||||
"""Constructor for IngressRequirer.
|
||||
|
||||
The request args can be used to specify the ingress properties when the
|
||||
instance is created. If any are set, at least `port` is required, and
|
||||
they will be sent to the ingress provider as soon as it is available.
|
||||
All request args must be given as keyword args.
|
||||
|
||||
Args:
|
||||
charm: the charm that is instantiating the library.
|
||||
relation_name: the name of the relation endpoint to bind to (defaults to `ingress`);
|
||||
relation must be of interface type `ingress` and have "limit: 1")
|
||||
host: Hostname to be used by the ingress provider to address the requiring
|
||||
application; if unspecified, the default Kubernetes service name will be used.
|
||||
strip_prefix: configure Traefik to strip the path prefix.
|
||||
redirect_https: redirect incoming requests to HTTPS.
|
||||
scheme: callable returning the scheme to use when constructing the ingress url.
|
||||
|
||||
Request Args:
|
||||
port: the port of the service
|
||||
"""
|
||||
super().__init__(charm, relation_name)
|
||||
self.charm: CharmBase = charm
|
||||
self.relation_name = relation_name
|
||||
self._strip_prefix = strip_prefix
|
||||
self._redirect_https = redirect_https
|
||||
self._get_scheme = scheme
|
||||
|
||||
self._stored.set_default(current_url=None) # type: ignore
|
||||
|
||||
# if instantiated with a port, and we are related, then
|
||||
# we immediately publish our ingress data to speed up the process.
|
||||
if port:
|
||||
self._auto_data = host, port
|
||||
else:
|
||||
self._auto_data = None
|
||||
|
||||
def _handle_relation(self, event):
|
||||
# created, joined or changed: if we have auto data: publish it
|
||||
self._publish_auto_data()
|
||||
|
||||
if self.is_ready():
|
||||
# Avoid spurious events, emit only when there is a NEW URL available
|
||||
new_url = (
|
||||
None
|
||||
if isinstance(event, RelationBrokenEvent)
|
||||
else self._get_url_from_relation_data()
|
||||
)
|
||||
if self._stored.current_url != new_url: # type: ignore
|
||||
self._stored.current_url = new_url # type: ignore
|
||||
self.on.ready.emit(event.relation, new_url) # type: ignore
|
||||
|
||||
def _handle_relation_broken(self, event):
|
||||
self._stored.current_url = None # type: ignore
|
||||
self.on.revoked.emit(event.relation) # type: ignore
|
||||
|
||||
def _handle_upgrade_or_leader(self, event):
|
||||
"""On upgrade/leadership change: ensure we publish the data we have."""
|
||||
self._publish_auto_data()
|
||||
|
||||
def is_ready(self):
|
||||
"""The Requirer is ready if the Provider has sent valid data."""
|
||||
try:
|
||||
return bool(self._get_url_from_relation_data())
|
||||
except DataValidationError as e:
|
||||
log.debug("Requirer not ready; validation error encountered: %s" % str(e))
|
||||
return False
|
||||
|
||||
def _publish_auto_data(self):
|
||||
if self._auto_data:
|
||||
host, port = self._auto_data
|
||||
self.provide_ingress_requirements(host=host, port=port)
|
||||
|
||||
def provide_ingress_requirements(
|
||||
self,
|
||||
*,
|
||||
scheme: Optional[str] = None,
|
||||
host: Optional[str] = None,
|
||||
port: int,
|
||||
):
|
||||
"""Publishes the data that Traefik needs to provide ingress.
|
||||
|
||||
Args:
|
||||
scheme: Scheme to be used; if unspecified, use the one used by __init__.
|
||||
host: Hostname to be used by the ingress provider to address the
|
||||
requirer unit; if unspecified, FQDN will be used instead
|
||||
port: the port of the service (required)
|
||||
"""
|
||||
for relation in self.relations:
|
||||
self._provide_ingress_requirements(scheme, host, port, relation)
|
||||
|
||||
def _provide_ingress_requirements(
|
||||
self,
|
||||
scheme: Optional[str],
|
||||
host: Optional[str],
|
||||
port: int,
|
||||
relation: Relation,
|
||||
):
|
||||
if self.unit.is_leader():
|
||||
self._publish_app_data(scheme, port, relation)
|
||||
|
||||
self._publish_unit_data(host, relation)
|
||||
|
||||
def _publish_unit_data(
|
||||
self,
|
||||
host: Optional[str],
|
||||
relation: Relation,
|
||||
):
|
||||
if not host:
|
||||
host = socket.getfqdn()
|
||||
|
||||
unit_databag = relation.data[self.unit]
|
||||
try:
|
||||
IngressRequirerUnitData(host=host).dump(unit_databag)
|
||||
except pydantic.ValidationError as e:
|
||||
msg = "failed to validate unit data"
|
||||
log.info(msg, exc_info=True) # log to INFO because this might be expected
|
||||
raise DataValidationError(msg) from e
|
||||
|
||||
def _publish_app_data(
|
||||
self,
|
||||
scheme: Optional[str],
|
||||
port: int,
|
||||
relation: Relation,
|
||||
):
|
||||
# assumes leadership!
|
||||
app_databag = relation.data[self.app]
|
||||
|
||||
if not scheme:
|
||||
# If scheme was not provided, use the one given to the constructor.
|
||||
scheme = self._get_scheme()
|
||||
|
||||
try:
|
||||
IngressRequirerAppData( # type: ignore # pyright does not like aliases
|
||||
model=self.model.name,
|
||||
name=self.app.name,
|
||||
scheme=scheme,
|
||||
port=port,
|
||||
strip_prefix=self._strip_prefix, # type: ignore # pyright does not like aliases
|
||||
redirect_https=self._redirect_https, # type: ignore # pyright does not like aliases
|
||||
).dump(app_databag)
|
||||
except pydantic.ValidationError as e:
|
||||
msg = "failed to validate app data"
|
||||
log.info(msg, exc_info=True) # log to INFO because this might be expected
|
||||
raise DataValidationError(msg) from e
|
||||
|
||||
@property
|
||||
def relation(self):
|
||||
"""The established Relation instance, or None."""
|
||||
return self.relations[0] if self.relations else None
|
||||
|
||||
def _get_url_from_relation_data(self) -> Optional[str]:
|
||||
"""The full ingress URL to reach the current unit.
|
||||
|
||||
Returns None if the URL isn't available yet.
|
||||
"""
|
||||
relation = self.relation
|
||||
if not relation or not relation.app:
|
||||
return None
|
||||
|
||||
# fetch the provider's app databag
|
||||
try:
|
||||
databag = relation.data[relation.app]
|
||||
except ModelError as e:
|
||||
log.debug(
|
||||
f"Error {e} attempting to read remote app data; "
|
||||
f"probably we are in a relation_departed hook"
|
||||
)
|
||||
return None
|
||||
|
||||
if not databag: # not ready yet
|
||||
return None
|
||||
|
||||
return str(IngressProviderAppData.load(databag).ingress.url)
|
||||
|
||||
@property
|
||||
def url(self) -> Optional[str]:
|
||||
"""The full ingress URL to reach the current unit.
|
||||
|
||||
Returns None if the URL isn't available yet.
|
||||
"""
|
||||
data = (
|
||||
typing.cast(Optional[str], self._stored.current_url) # type: ignore
|
||||
or self._get_url_from_relation_data()
|
||||
)
|
||||
return data
|
@ -1,10 +0,0 @@
|
||||
- project:
|
||||
templates:
|
||||
- charm-publish-jobs
|
||||
vars:
|
||||
needs_charm_build: true
|
||||
charm_build_name: barbican-k8s
|
||||
build_type: charmcraft
|
||||
publish_charm: true
|
||||
charmcraft_channel: 2.0/stable
|
||||
publish_channel: 2023.2/edge
|
@ -1,39 +0,0 @@
|
||||
# Copyright 2022 Canonical Ltd.
|
||||
# See LICENSE file for licensing details.
|
||||
|
||||
# Testing tools configuration
|
||||
[tool.coverage.run]
|
||||
branch = true
|
||||
|
||||
[tool.coverage.report]
|
||||
show_missing = true
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
minversion = "6.0"
|
||||
log_cli_level = "INFO"
|
||||
|
||||
# Formatting tools configuration
|
||||
[tool.black]
|
||||
line-length = 79
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
multi_line_output = 3
|
||||
force_grid_wrap = true
|
||||
|
||||
# Linting tools configuration
|
||||
[tool.flake8]
|
||||
max-line-length = 79
|
||||
max-doc-length = 99
|
||||
max-complexity = 10
|
||||
exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"]
|
||||
select = ["E", "W", "F", "C", "N", "R", "D", "H"]
|
||||
# Ignore W503, E501 because using black creates errors with this
|
||||
# Ignore D107 Missing docstring in __init__
|
||||
ignore = ["W503", "E501", "D107", "E402"]
|
||||
per-file-ignores = []
|
||||
docstring-convention = "google"
|
||||
# Check for properly formatted copyright header in each file
|
||||
copyright-check = "True"
|
||||
copyright-author = "Canonical Ltd."
|
||||
copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s"
|
@ -1,13 +0,0 @@
|
||||
#!/bin/bash
|
||||
charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}')
|
||||
echo "renaming ${charm}_*.charm to ${charm}.charm"
|
||||
echo -n "pwd: "
|
||||
pwd
|
||||
ls -al
|
||||
echo "Removing bad downloaded charm maybe?"
|
||||
if [[ -e "${charm}.charm" ]];
|
||||
then
|
||||
rm "${charm}.charm"
|
||||
fi
|
||||
echo "Renaming charm here."
|
||||
mv ${charm}_*.charm ${charm}.charm
|
@ -13,4 +13,6 @@ lightkube-models
|
||||
ops
|
||||
pwgen
|
||||
pytest-interface-tester
|
||||
git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
|
||||
|
||||
# From ops_sunbeam
|
||||
tenacity
|
||||
|
@ -10,6 +10,8 @@ sql_connection = {{ database.connection }}
|
||||
db_auto_create = false
|
||||
|
||||
{% include "parts/section-identity" %}
|
||||
# XXX Region should come from the id relation here
|
||||
region_name = {{ options.region }}
|
||||
|
||||
{% include "parts/section-service-user" %}
|
||||
|
||||
|
@ -1,27 +0,0 @@
|
||||
[keystone_authtoken]
|
||||
{% if identity_service.admin_auth_url -%}
|
||||
auth_url = {{ identity_service.admin_auth_url }}
|
||||
interface = admin
|
||||
{% elif identity_service.internal_auth_url -%}
|
||||
auth_url = {{ identity_service.internal_auth_url }}
|
||||
interface = internal
|
||||
{% elif identity_service.internal_host -%}
|
||||
auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
|
||||
interface = internal
|
||||
{% endif -%}
|
||||
{% if identity_service.public_auth_url -%}
|
||||
www_authenticate_uri = {{ identity_service.public_auth_url }}
|
||||
{% elif identity_service.internal_host -%}
|
||||
www_authenticate_uri = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
|
||||
{% endif -%}
|
||||
auth_type = password
|
||||
project_domain_name = {{ identity_service.service_domain_name }}
|
||||
user_domain_name = {{ identity_service.service_domain_name }}
|
||||
project_name = {{ identity_service.service_project_name }}
|
||||
username = {{ identity_service.service_user_name }}
|
||||
password = {{ identity_service.service_password }}
|
||||
service_token_roles = {{ identity_service.admin_role }}
|
||||
service_token_roles_required = True
|
||||
|
||||
# XXX Region should come from the id relation here
|
||||
region_name = {{ options.region }}
|
@ -1,2 +0,0 @@
|
||||
[oslo_messaging_rabbit]
|
||||
rabbit_quorum_queue = True
|
@ -1,14 +0,0 @@
|
||||
# This file is managed centrally by release-tools and should not be modified
|
||||
# within individual charm repos. See the 'global' dir contents for available
|
||||
# choices of *requirements.txt files for OpenStack Charms:
|
||||
# https://github.com/openstack-charmers/release-tools
|
||||
#
|
||||
|
||||
coverage
|
||||
mock
|
||||
flake8
|
||||
stestr
|
||||
git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza
|
||||
git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack
|
||||
git+https://opendev.org/openstack/tempest.git#egg=tempest
|
||||
ops
|
@ -16,9 +16,8 @@
|
||||
|
||||
"""Unit tests for Barbican operator."""
|
||||
|
||||
import ops_sunbeam.test_utils as test_utils
|
||||
|
||||
import charm
|
||||
import ops_sunbeam.test_utils as test_utils
|
||||
|
||||
|
||||
class _BarbicanTestOperatorCharm(charm.BarbicanOperatorCharm):
|
||||
|
@ -1,169 +0,0 @@
|
||||
# Source charm: ./tox.ini
|
||||
# This file is managed centrally by release-tools and should not be modified
|
||||
# within individual charm repos. See the 'global' dir contents for available
|
||||
# choices of tox.ini for OpenStack Charms:
|
||||
# https://github.com/openstack-charmers/release-tools
|
||||
|
||||
[tox]
|
||||
skipsdist = True
|
||||
envlist = pep8,py3
|
||||
sitepackages = False
|
||||
skip_missing_interpreters = False
|
||||
minversion = 3.18.0
|
||||
|
||||
[vars]
|
||||
src_path = {toxinidir}/src/
|
||||
tst_path = {toxinidir}/tests/
|
||||
lib_path = {toxinidir}/lib/
|
||||
pyproject_toml = {toxinidir}/pyproject.toml
|
||||
all_path = {[vars]src_path} {[vars]tst_path}
|
||||
|
||||
[testenv]
|
||||
basepython = python3
|
||||
setenv =
|
||||
PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path}
|
||||
passenv =
|
||||
HOME
|
||||
PYTHONPATH
|
||||
install_command =
|
||||
pip install {opts} {packages}
|
||||
commands = stestr run --slowest {posargs}
|
||||
allowlist_externals =
|
||||
git
|
||||
charmcraft
|
||||
{toxinidir}/fetch-libs.sh
|
||||
{toxinidir}/rename.sh
|
||||
deps =
|
||||
-r{toxinidir}/test-requirements.txt
|
||||
|
||||
[testenv:fmt]
|
||||
description = Apply coding style standards to code
|
||||
deps =
|
||||
black
|
||||
isort
|
||||
commands =
|
||||
isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox
|
||||
black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path}
|
||||
|
||||
[testenv:build]
|
||||
basepython = python3
|
||||
deps =
|
||||
commands =
|
||||
charmcraft -v pack
|
||||
{toxinidir}/rename.sh
|
||||
|
||||
[testenv:fetch]
|
||||
basepython = python3
|
||||
deps =
|
||||
commands =
|
||||
{toxinidir}/fetch-libs.sh
|
||||
|
||||
[testenv:py3]
|
||||
basepython = python3
|
||||
deps =
|
||||
{[testenv]deps}
|
||||
-r{toxinidir}/requirements.txt
|
||||
|
||||
[testenv:py38]
|
||||
basepython = python3.8
|
||||
deps = {[testenv:py3]deps}
|
||||
|
||||
[testenv:py39]
|
||||
basepython = python3.9
|
||||
deps = {[testenv:py3]deps}
|
||||
|
||||
[testenv:py310]
|
||||
basepython = python3.10
|
||||
deps = {[testenv:py3]deps}
|
||||
|
||||
[testenv:py311]
|
||||
basepython = python3.11
|
||||
deps = {[testenv:py3]deps}
|
||||
|
||||
[testenv:cover]
|
||||
basepython = python3
|
||||
deps = {[testenv:py3]deps}
|
||||
setenv =
|
||||
{[testenv]setenv}
|
||||
PYTHON=coverage run
|
||||
commands =
|
||||
coverage erase
|
||||
stestr run --slowest {posargs}
|
||||
coverage combine
|
||||
coverage html -d cover
|
||||
coverage xml -o cover/coverage.xml
|
||||
coverage report
|
||||
|
||||
[testenv:pep8]
|
||||
description = Alias for lint
|
||||
deps = {[testenv:lint]deps}
|
||||
commands = {[testenv:lint]commands}
|
||||
|
||||
[testenv:lint]
|
||||
description = Check code against coding style standards
|
||||
deps =
|
||||
black
|
||||
flake8<6 # Pin version until https://github.com/savoirfairelinux/flake8-copyright/issues/19 is merged
|
||||
flake8-docstrings
|
||||
flake8-copyright
|
||||
flake8-builtins
|
||||
pyproject-flake8
|
||||
pep8-naming
|
||||
isort
|
||||
codespell
|
||||
commands =
|
||||
codespell {[vars]all_path}
|
||||
# pflake8 wrapper supports config from pyproject.toml
|
||||
pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path}
|
||||
isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path}
|
||||
black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path}
|
||||
|
||||
[testenv:func-noop]
|
||||
basepython = python3
|
||||
deps =
|
||||
git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza
|
||||
git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack
|
||||
git+https://opendev.org/openstack/tempest.git#egg=tempest
|
||||
commands =
|
||||
functest-run-suite --help
|
||||
|
||||
[testenv:func]
|
||||
basepython = python3
|
||||
deps = {[testenv:func-noop]deps}
|
||||
commands =
|
||||
functest-run-suite --keep-model
|
||||
|
||||
[testenv:func-smoke]
|
||||
basepython = python3
|
||||
deps = {[testenv:func-noop]deps}
|
||||
setenv =
|
||||
TEST_MODEL_SETTINGS = automatically-retry-hooks=true
|
||||
TEST_MAX_RESOLVE_COUNT = 5
|
||||
commands =
|
||||
functest-run-suite --keep-model --smoke
|
||||
|
||||
[testenv:func-dev]
|
||||
basepython = python3
|
||||
deps = {[testenv:func-noop]deps}
|
||||
commands =
|
||||
functest-run-suite --keep-model --dev
|
||||
|
||||
[testenv:func-target]
|
||||
basepython = python3
|
||||
deps = {[testenv:func-noop]deps}
|
||||
commands =
|
||||
functest-run-suite --keep-model --bundle {posargs}
|
||||
|
||||
[coverage:run]
|
||||
branch = True
|
||||
concurrency = multiprocessing
|
||||
parallel = True
|
||||
source =
|
||||
.
|
||||
omit =
|
||||
.tox/*
|
||||
tests/*
|
||||
src/templates/*
|
||||
|
||||
[flake8]
|
||||
ignore=E226,W504
|
11
charms/ceilometer-k8s/.gitignore
vendored
11
charms/ceilometer-k8s/.gitignore
vendored
@ -1,11 +0,0 @@
|
||||
venv/
|
||||
build/
|
||||
*.charm
|
||||
.tox/
|
||||
.coverage
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
.idea
|
||||
.vscode/
|
||||
*.swp
|
||||
.stestr/
|
@ -1,5 +0,0 @@
|
||||
[gerrit]
|
||||
host=review.opendev.org
|
||||
port=29418
|
||||
project=openstack/charm-ceilometer-k8s.git
|
||||
defaultbranch=main
|
@ -1,3 +0,0 @@
|
||||
[DEFAULT]
|
||||
test_path=./tests/unit
|
||||
top_dir=./tests
|
@ -1,11 +0,0 @@
|
||||
- project:
|
||||
templates:
|
||||
- openstack-python3-charm-jobs
|
||||
- openstack-cover-jobs
|
||||
- microk8s-func-test
|
||||
vars:
|
||||
charm_build_name: ceilometer-k8s
|
||||
juju_channel: 3.1/stable
|
||||
juju_classic_mode: false
|
||||
microk8s_channel: 1.28-strict/stable
|
||||
microk8s_classic_mode: false
|
@ -27,4 +27,3 @@ parts:
|
||||
- cryptography
|
||||
- jsonschema
|
||||
- jinja2
|
||||
- git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
|
||||
|
@ -1,8 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "INFO: Fetching libs from charmhub."
|
||||
charmcraft fetch-lib charms.gnocchi_k8s.v0.gnocchi_service
|
||||
# charmcraft fetch-lib charms.data_platform_libs.v0.database_requires
|
||||
# charmcraft fetch-lib charms.keystone_k8s.v1.identity_service
|
||||
# charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq
|
||||
# charmcraft fetch-lib charms.traefik_k8s.v1.ingress
|
@ -1,286 +0,0 @@
|
||||
"""RabbitMQProvides and Requires module.
|
||||
|
||||
This library contains the Requires and Provides classes for handling
|
||||
the rabbitmq interface.
|
||||
|
||||
Import `RabbitMQRequires` in your charm, with the charm object and the
|
||||
relation name:
|
||||
- self
|
||||
- "amqp"
|
||||
|
||||
Also provide two additional parameters to the charm object:
|
||||
- username
|
||||
- vhost
|
||||
|
||||
Two events are also available to respond to:
|
||||
- connected
|
||||
- ready
|
||||
- goneaway
|
||||
|
||||
A basic example showing the usage of this relation follows:
|
||||
|
||||
```
|
||||
from charms.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires
|
||||
|
||||
class RabbitMQClientCharm(CharmBase):
|
||||
def __init__(self, *args):
|
||||
super().__init__(*args)
|
||||
# RabbitMQ Requires
|
||||
self.amqp = RabbitMQRequires(
|
||||
self, "amqp",
|
||||
username="myusername",
|
||||
vhost="vhostname"
|
||||
)
|
||||
self.framework.observe(
|
||||
self.amqp.on.connected, self._on_amqp_connected)
|
||||
self.framework.observe(
|
||||
self.amqp.on.ready, self._on_amqp_ready)
|
||||
self.framework.observe(
|
||||
self.amqp.on.goneaway, self._on_amqp_goneaway)
|
||||
|
||||
def _on_amqp_connected(self, event):
|
||||
'''React to the RabbitMQ connected event.
|
||||
|
||||
This event happens when n RabbitMQ relation is added to the
|
||||
model before credentials etc have been provided.
|
||||
'''
|
||||
# Do something before the relation is complete
|
||||
pass
|
||||
|
||||
def _on_amqp_ready(self, event):
|
||||
'''React to the RabbitMQ ready event.
|
||||
|
||||
The RabbitMQ interface will use the provided username and vhost for the
|
||||
request to the rabbitmq server.
|
||||
'''
|
||||
# RabbitMQ Relation is ready. Do something with the completed relation.
|
||||
pass
|
||||
|
||||
def _on_amqp_goneaway(self, event):
|
||||
'''React to the RabbitMQ goneaway event.
|
||||
|
||||
This event happens when an RabbitMQ relation is removed.
|
||||
'''
|
||||
# RabbitMQ Relation has goneaway. shutdown services or suchlike
|
||||
pass
|
||||
```
|
||||
"""
|
||||
|
||||
# The unique Charmhub library identifier, never change it
|
||||
LIBID = "45622352791142fd9cf87232e3bd6f2a"
|
||||
|
||||
# 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
|
||||
|
||||
import logging
|
||||
|
||||
from ops.framework import (
|
||||
StoredState,
|
||||
EventBase,
|
||||
ObjectEvents,
|
||||
EventSource,
|
||||
Object,
|
||||
)
|
||||
|
||||
from ops.model import Relation
|
||||
|
||||
from typing import List
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RabbitMQConnectedEvent(EventBase):
|
||||
"""RabbitMQ connected Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class RabbitMQReadyEvent(EventBase):
|
||||
"""RabbitMQ ready for use Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class RabbitMQGoneAwayEvent(EventBase):
|
||||
"""RabbitMQ relation has gone-away Event"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class RabbitMQServerEvents(ObjectEvents):
|
||||
"""Events class for `on`"""
|
||||
|
||||
connected = EventSource(RabbitMQConnectedEvent)
|
||||
ready = EventSource(RabbitMQReadyEvent)
|
||||
goneaway = EventSource(RabbitMQGoneAwayEvent)
|
||||
|
||||
|
||||
class RabbitMQRequires(Object):
|
||||
"""
|
||||
RabbitMQRequires class
|
||||
"""
|
||||
|
||||
on = RabbitMQServerEvents()
|
||||
|
||||
def __init__(self, charm, relation_name: str, username: str, vhost: str):
|
||||
super().__init__(charm, relation_name)
|
||||
self.charm = charm
|
||||
self.relation_name = relation_name
|
||||
self.username = username
|
||||
self.vhost = vhost
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_joined,
|
||||
self._on_amqp_relation_joined,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_changed,
|
||||
self._on_amqp_relation_changed,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_departed,
|
||||
self._on_amqp_relation_changed,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_broken,
|
||||
self._on_amqp_relation_broken,
|
||||
)
|
||||
|
||||
def _on_amqp_relation_joined(self, event):
|
||||
"""RabbitMQ relation joined."""
|
||||
logging.debug("RabbitMQRabbitMQRequires on_joined")
|
||||
self.on.connected.emit()
|
||||
self.request_access(self.username, self.vhost)
|
||||
|
||||
def _on_amqp_relation_changed(self, event):
|
||||
"""RabbitMQ relation changed."""
|
||||
logging.debug("RabbitMQRabbitMQRequires on_changed/departed")
|
||||
if self.password:
|
||||
self.on.ready.emit()
|
||||
|
||||
def _on_amqp_relation_broken(self, event):
|
||||
"""RabbitMQ relation broken."""
|
||||
logging.debug("RabbitMQRabbitMQRequires on_broken")
|
||||
self.on.goneaway.emit()
|
||||
|
||||
@property
|
||||
def _amqp_rel(self) -> Relation:
|
||||
"""The RabbitMQ relation."""
|
||||
return self.framework.model.get_relation(self.relation_name)
|
||||
|
||||
@property
|
||||
def password(self) -> str:
|
||||
"""Return the RabbitMQ password from the server side of the relation."""
|
||||
return self._amqp_rel.data[self._amqp_rel.app].get("password")
|
||||
|
||||
@property
|
||||
def hostname(self) -> str:
|
||||
"""Return the hostname from the RabbitMQ relation"""
|
||||
return self._amqp_rel.data[self._amqp_rel.app].get("hostname")
|
||||
|
||||
@property
|
||||
def ssl_port(self) -> str:
|
||||
"""Return the SSL port from the RabbitMQ relation"""
|
||||
return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port")
|
||||
|
||||
@property
|
||||
def ssl_ca(self) -> str:
|
||||
"""Return the SSL port from the RabbitMQ relation"""
|
||||
return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca")
|
||||
|
||||
@property
|
||||
def hostnames(self) -> List[str]:
|
||||
"""Return a list of remote RMQ hosts from the RabbitMQ relation"""
|
||||
_hosts = []
|
||||
for unit in self._amqp_rel.units:
|
||||
_hosts.append(self._amqp_rel.data[unit].get("ingress-address"))
|
||||
return _hosts
|
||||
|
||||
def request_access(self, username: str, vhost: str) -> None:
|
||||
"""Request access to the RabbitMQ server."""
|
||||
if self.model.unit.is_leader():
|
||||
logging.debug("Requesting RabbitMQ user and vhost")
|
||||
self._amqp_rel.data[self.charm.app]["username"] = username
|
||||
self._amqp_rel.data[self.charm.app]["vhost"] = vhost
|
||||
|
||||
|
||||
class HasRabbitMQClientsEvent(EventBase):
|
||||
"""Has RabbitMQClients Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ReadyRabbitMQClientsEvent(EventBase):
|
||||
"""RabbitMQClients Ready Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class RabbitMQClientEvents(ObjectEvents):
|
||||
"""Events class for `on`"""
|
||||
|
||||
has_amqp_clients = EventSource(HasRabbitMQClientsEvent)
|
||||
ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent)
|
||||
|
||||
|
||||
class RabbitMQProvides(Object):
|
||||
"""
|
||||
RabbitMQProvides class
|
||||
"""
|
||||
|
||||
on = RabbitMQClientEvents()
|
||||
|
||||
def __init__(self, charm, relation_name, callback):
|
||||
super().__init__(charm, relation_name)
|
||||
self.charm = charm
|
||||
self.relation_name = relation_name
|
||||
self.callback = callback
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_joined,
|
||||
self._on_amqp_relation_joined,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_changed,
|
||||
self._on_amqp_relation_changed,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_broken,
|
||||
self._on_amqp_relation_broken,
|
||||
)
|
||||
|
||||
def _on_amqp_relation_joined(self, event):
|
||||
"""Handle RabbitMQ joined."""
|
||||
logging.debug("RabbitMQRabbitMQProvides on_joined data={}"
|
||||
.format(event.relation.data[event.relation.app]))
|
||||
self.on.has_amqp_clients.emit()
|
||||
|
||||
def _on_amqp_relation_changed(self, event):
|
||||
"""Handle RabbitMQ changed."""
|
||||
logging.debug("RabbitMQRabbitMQProvides on_changed data={}"
|
||||
.format(event.relation.data[event.relation.app]))
|
||||
# Validate data on the relation
|
||||
if self.username(event) and self.vhost(event):
|
||||
self.on.ready_amqp_clients.emit()
|
||||
if self.charm.unit.is_leader():
|
||||
self.callback(event, self.username(event), self.vhost(event))
|
||||
else:
|
||||
logging.warning("Received RabbitMQ changed event without the "
|
||||
"expected keys ('username', 'vhost') in the "
|
||||
"application data bag. Incompatible charm in "
|
||||
"other end of relation?")
|
||||
|
||||
def _on_amqp_relation_broken(self, event):
|
||||
"""Handle RabbitMQ broken."""
|
||||
logging.debug("RabbitMQRabbitMQProvides on_departed")
|
||||
# TODO clear data on the relation
|
||||
|
||||
def username(self, event):
|
||||
"""Return the RabbitMQ username from the client side of the relation."""
|
||||
return event.relation.data[event.relation.app].get("username")
|
||||
|
||||
def vhost(self, event):
|
||||
"""Return the RabbitMQ vhost from the client side of the relation."""
|
||||
return event.relation.data[event.relation.app].get("vhost")
|
@ -1,10 +0,0 @@
|
||||
- project:
|
||||
templates:
|
||||
- charm-publish-jobs
|
||||
vars:
|
||||
needs_charm_build: true
|
||||
charm_build_name: ceilometer-k8s
|
||||
build_type: charmcraft
|
||||
publish_charm: true
|
||||
charmcraft_channel: 2.0/stable
|
||||
publish_channel: 2023.2/edge
|
@ -1,39 +0,0 @@
|
||||
# Copyright 2023 Canonical Ltd.
|
||||
# See LICENSE file for licensing details.
|
||||
|
||||
# Testing tools configuration
|
||||
[tool.coverage.run]
|
||||
branch = true
|
||||
|
||||
[tool.coverage.report]
|
||||
show_missing = true
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
minversion = "6.0"
|
||||
log_cli_level = "INFO"
|
||||
|
||||
# Formatting tools configuration
|
||||
[tool.black]
|
||||
line-length = 79
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
multi_line_output = 3
|
||||
force_grid_wrap = true
|
||||
|
||||
# Linting tools configuration
|
||||
[tool.flake8]
|
||||
max-line-length = 79
|
||||
max-doc-length = 99
|
||||
max-complexity = 10
|
||||
exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"]
|
||||
select = ["E", "W", "F", "C", "N", "R", "D", "H"]
|
||||
# Ignore W503, E501 because using black creates errors with this
|
||||
# Ignore D107 Missing docstring in __init__
|
||||
ignore = ["W503", "E501", "D107", "E402"]
|
||||
per-file-ignores = []
|
||||
docstring-convention = "google"
|
||||
# Check for properly formatted copyright header in each file
|
||||
copyright-check = "True"
|
||||
copyright-author = "Canonical Ltd."
|
||||
copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s"
|
@ -1,13 +0,0 @@
|
||||
#!/bin/bash
|
||||
charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}')
|
||||
echo "renaming ${charm}_*.charm to ${charm}.charm"
|
||||
echo -n "pwd: "
|
||||
pwd
|
||||
ls -al
|
||||
echo "Removing bad downloaded charm maybe?"
|
||||
if [[ -e "${charm}.charm" ]];
|
||||
then
|
||||
rm "${charm}.charm"
|
||||
fi
|
||||
echo "Renaming charm here."
|
||||
mv ${charm}_*.charm ${charm}.charm
|
@ -1,8 +1,10 @@
|
||||
ops
|
||||
jinja2
|
||||
git+https://github.com/openstack/charm-ops-sunbeam#egg=ops_sunbeam
|
||||
lightkube
|
||||
|
||||
# Uncomment below if charm relates to ceph
|
||||
# git+https://github.com/openstack/charm-ops-interface-ceph-client#egg=interface_ceph_client
|
||||
# git+https://github.com/juju/charm-helpers.git#egg=charmhelpers
|
||||
|
||||
# From ops_sunbeam
|
||||
tenacity
|
||||
|
@ -20,8 +20,8 @@ archive_policy = low
|
||||
[keystone_authtoken]
|
||||
{% include "parts/identity-data-id-creds" %}
|
||||
|
||||
{% include "parts/section-service-user-id-creds" %}
|
||||
{% include "parts/section-service-user-from-identity-credentials" %}
|
||||
|
||||
{% include "parts/section-service-credentials" %}
|
||||
{% include "parts/section-service-credentials-from-identity-service" %}
|
||||
|
||||
{% include "parts/section-oslo-messaging-rabbit" %}
|
||||
|
@ -1,3 +0,0 @@
|
||||
{% if database.connection -%}
|
||||
connection = {{ database.connection }}
|
||||
{% endif -%}
|
@ -1,23 +0,0 @@
|
||||
{% if identity_service.admin_auth_url -%}
|
||||
auth_url = {{ identity_service.admin_auth_url }}
|
||||
interface = admin
|
||||
{% elif identity_service.internal_auth_url -%}
|
||||
auth_url = {{ identity_service.internal_auth_url }}
|
||||
interface = internal
|
||||
{% elif identity_service.internal_host -%}
|
||||
auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
|
||||
interface = internal
|
||||
{% endif -%}
|
||||
{% if identity_service.public_auth_url -%}
|
||||
www_authenticate_uri = {{ identity_service.public_auth_url }}
|
||||
{% elif identity_service.internal_host -%}
|
||||
www_authenticate_uri = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
|
||||
{% endif -%}
|
||||
auth_type = password
|
||||
project_domain_name = {{ identity_service.service_domain_name }}
|
||||
user_domain_name = {{ identity_service.service_domain_name }}
|
||||
project_name = {{ identity_service.service_project_name }}
|
||||
username = {{ identity_service.service_user_name }}
|
||||
password = {{ identity_service.service_password }}
|
||||
service_token_roles = {{ identity_service.admin_role }}
|
||||
service_token_roles_required = True
|
@ -1,3 +0,0 @@
|
||||
[database]
|
||||
{% include "parts/database-connection" %}
|
||||
connection_recycle_time = 200
|
@ -1,10 +0,0 @@
|
||||
{% if trusted_dashboards %}
|
||||
[federation]
|
||||
{% for dashboard_url in trusted_dashboards -%}
|
||||
trusted_dashboard = {{ dashboard_url }}
|
||||
{% endfor -%}
|
||||
{% endif %}
|
||||
{% for sp in fid_sps -%}
|
||||
[{{ sp['protocol-name'] }}]
|
||||
remote_id_attribute = {{ sp['remote-id-attribute'] }}
|
||||
{% endfor -%}
|
@ -1,2 +0,0 @@
|
||||
[keystone_authtoken]
|
||||
{% include "parts/identity-data" %}
|
@ -1,6 +0,0 @@
|
||||
{% for section in sections -%}
|
||||
[{{section}}]
|
||||
{% for key, value in sections[section].items() -%}
|
||||
{{ key }} = {{ value }}
|
||||
{% endfor %}
|
||||
{%- endfor %}
|
@ -1,2 +0,0 @@
|
||||
[oslo_messaging_rabbit]
|
||||
rabbit_quorum_queue = True
|
@ -1,15 +0,0 @@
|
||||
{% if identity_service.service_domain_id -%}
|
||||
[service_user]
|
||||
{% if identity_service.internal_auth_url -%}
|
||||
auth_url = {{ identity_service.internal_auth_url }}
|
||||
{% elif identity_service.internal_host -%}
|
||||
auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
|
||||
{% endif -%}
|
||||
send_service_user_token = true
|
||||
auth_type = password
|
||||
project_domain_id = {{ identity_service.service_domain_id }}
|
||||
user_domain_id = {{ identity_service.service_domain_id }}
|
||||
project_name = {{ identity_service.service_project_name }}
|
||||
username = {{ identity_service.service_user_name }}
|
||||
password = {{ identity_service.service_password }}
|
||||
{% endif -%}
|
@ -1,15 +0,0 @@
|
||||
{% if enable_signing -%}
|
||||
[signing]
|
||||
{% if certfile -%}
|
||||
certfile = {{ certfile }}
|
||||
{% endif -%}
|
||||
{% if keyfile -%}
|
||||
keyfile = {{ keyfile }}
|
||||
{% endif -%}
|
||||
{% if ca_certs -%}
|
||||
ca_certs = {{ ca_certs }}
|
||||
{% endif -%}
|
||||
{% if ca_key -%}
|
||||
ca_key = {{ ca_key }}
|
||||
{% endif -%}
|
||||
{% endif -%}
|
@ -1,11 +0,0 @@
|
||||
# This file is managed centrally. If you find the need to modify this as a
|
||||
# one-off, please don't. Intead, consult #openstack-charms and ask about
|
||||
# requirements management in charms via bot-control. Thank you.
|
||||
|
||||
coverage
|
||||
mock
|
||||
flake8
|
||||
stestr
|
||||
ops
|
||||
# Subunit 1.4.3+ requires extras
|
||||
extras
|
@ -16,9 +16,8 @@
|
||||
|
||||
"""Tests for gnocchi charm."""
|
||||
|
||||
import ops_sunbeam.test_utils as test_utils
|
||||
|
||||
import charm
|
||||
import ops_sunbeam.test_utils as test_utils
|
||||
|
||||
|
||||
class _CeilometerOperatorCharm(charm.CeilometerOperatorCharm):
|
||||
|
@ -1,165 +0,0 @@
|
||||
# Operator charm (with zaza): tox.ini
|
||||
|
||||
[tox]
|
||||
skipsdist = True
|
||||
envlist = pep8,py3
|
||||
sitepackages = False
|
||||
skip_missing_interpreters = False
|
||||
minversion = 3.18.0
|
||||
|
||||
[vars]
|
||||
src_path = {toxinidir}/src/
|
||||
tst_path = {toxinidir}/tests/
|
||||
lib_path = {toxinidir}/lib/
|
||||
pyproject_toml = {toxinidir}/pyproject.toml
|
||||
all_path = {[vars]src_path} {[vars]tst_path}
|
||||
|
||||
[testenv]
|
||||
basepython = python3
|
||||
setenv =
|
||||
PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path}
|
||||
passenv =
|
||||
HOME
|
||||
PYTHONPATH
|
||||
install_command =
|
||||
pip install {opts} {packages}
|
||||
commands = stestr run --slowest {posargs}
|
||||
allowlist_externals =
|
||||
git
|
||||
charmcraft
|
||||
{toxinidir}/fetch-libs.sh
|
||||
{toxinidir}/rename.sh
|
||||
deps =
|
||||
-r{toxinidir}/test-requirements.txt
|
||||
|
||||
[testenv:fmt]
|
||||
description = Apply coding style standards to code
|
||||
deps =
|
||||
black
|
||||
isort
|
||||
commands =
|
||||
isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox
|
||||
black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path}
|
||||
|
||||
[testenv:build]
|
||||
basepython = python3
|
||||
deps =
|
||||
commands =
|
||||
charmcraft -v pack
|
||||
{toxinidir}/rename.sh
|
||||
|
||||
[testenv:fetch]
|
||||
basepython = python3
|
||||
deps =
|
||||
commands =
|
||||
{toxinidir}/fetch-libs.sh
|
||||
|
||||
[testenv:py3]
|
||||
basepython = python3
|
||||
deps =
|
||||
{[testenv]deps}
|
||||
-r{toxinidir}/requirements.txt
|
||||
|
||||
[testenv:py38]
|
||||
basepython = python3.8
|
||||
deps = {[testenv:py3]deps}
|
||||
|
||||
[testenv:py39]
|
||||
basepython = python3.9
|
||||
deps = {[testenv:py3]deps}
|
||||
|
||||
[testenv:py310]
|
||||
basepython = python3.10
|
||||
deps = {[testenv:py3]deps}
|
||||
|
||||
[testenv:py311]
|
||||
basepython = python3.11
|
||||
deps = {[testenv:py3]deps}
|
||||
|
||||
[testenv:cover]
|
||||
basepython = python3
|
||||
deps = {[testenv:py3]deps}
|
||||
setenv =
|
||||
{[testenv]setenv}
|
||||
PYTHON=coverage run
|
||||
commands =
|
||||
coverage erase
|
||||
stestr run --slowest {posargs}
|
||||
coverage combine
|
||||
coverage html -d cover
|
||||
coverage xml -o cover/coverage.xml
|
||||
coverage report
|
||||
|
||||
[testenv:pep8]
|
||||
description = Alias for lint
|
||||
deps = {[testenv:lint]deps}
|
||||
commands = {[testenv:lint]commands}
|
||||
|
||||
[testenv:lint]
|
||||
description = Check code against coding style standards
|
||||
deps =
|
||||
black
|
||||
flake8<6 # Pin version until https://github.com/savoirfairelinux/flake8-copyright/issues/19 is merged
|
||||
flake8-docstrings
|
||||
flake8-copyright
|
||||
flake8-builtins
|
||||
pyproject-flake8
|
||||
pep8-naming
|
||||
isort
|
||||
codespell
|
||||
commands =
|
||||
codespell {[vars]all_path}
|
||||
# pflake8 wrapper supports config from pyproject.toml
|
||||
pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path}
|
||||
isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path}
|
||||
black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path}
|
||||
|
||||
[testenv:func-noop]
|
||||
basepython = python3
|
||||
deps =
|
||||
git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza
|
||||
git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack
|
||||
git+https://opendev.org/openstack/tempest.git#egg=tempest
|
||||
commands =
|
||||
functest-run-suite --help
|
||||
|
||||
[testenv:func]
|
||||
basepython = python3
|
||||
deps = {[testenv:func-noop]deps}
|
||||
commands =
|
||||
functest-run-suite --keep-model
|
||||
|
||||
[testenv:func-smoke]
|
||||
basepython = python3
|
||||
deps = {[testenv:func-noop]deps}
|
||||
setenv =
|
||||
TEST_MODEL_SETTINGS = automatically-retry-hooks=true
|
||||
TEST_MAX_RESOLVE_COUNT = 5
|
||||
commands =
|
||||
functest-run-suite --keep-model --smoke
|
||||
|
||||
[testenv:func-dev]
|
||||
basepython = python3
|
||||
deps = {[testenv:func-noop]deps}
|
||||
commands =
|
||||
functest-run-suite --keep-model --dev
|
||||
|
||||
[testenv:func-target]
|
||||
basepython = python3
|
||||
deps = {[testenv:func-noop]deps}
|
||||
commands =
|
||||
functest-run-suite --keep-model --bundle {posargs}
|
||||
|
||||
[coverage:run]
|
||||
branch = True
|
||||
concurrency = multiprocessing
|
||||
parallel = True
|
||||
source =
|
||||
.
|
||||
omit =
|
||||
.tox/*
|
||||
tests/*
|
||||
src/templates/*
|
||||
|
||||
[flake8]
|
||||
ignore=E226,W504
|
@ -1,9 +0,0 @@
|
||||
[flake8]
|
||||
max-line-length = 99
|
||||
select: E,W,F,C,N
|
||||
exclude:
|
||||
venv
|
||||
.git
|
||||
build
|
||||
dist
|
||||
*.egg_info
|
9
charms/cinder-ceph-k8s/.gitignore
vendored
9
charms/cinder-ceph-k8s/.gitignore
vendored
@ -1,9 +0,0 @@
|
||||
venv/
|
||||
build/
|
||||
*.charm
|
||||
|
||||
.coverage
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
.tox
|
||||
.stestr
|
@ -1,5 +0,0 @@
|
||||
[gerrit]
|
||||
host=review.opendev.org
|
||||
port=29418
|
||||
project=openstack/charm-cinder-ceph-k8s.git
|
||||
defaultbranch=main
|
@ -1,5 +0,0 @@
|
||||
/venv
|
||||
*.py[cod]
|
||||
*.charm
|
||||
.tox
|
||||
.stestr
|
@ -1,3 +0,0 @@
|
||||
[DEFAULT]
|
||||
test_path=./tests/unit
|
||||
top_dir=./tests
|
@ -1,11 +0,0 @@
|
||||
- project:
|
||||
templates:
|
||||
- openstack-python3-charm-jobs
|
||||
- openstack-cover-jobs
|
||||
- microk8s-func-test
|
||||
vars:
|
||||
charm_build_name: cinder-ceph-k8s
|
||||
juju_channel: 3.1/stable
|
||||
juju_classic_mode: false
|
||||
microk8s_channel: 1.28-strict/stable
|
||||
microk8s_classic_mode: false
|
@ -28,4 +28,3 @@ parts:
|
||||
- jsonschema
|
||||
- pydantic<2.0
|
||||
- jinja2
|
||||
- git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
|
||||
|
@ -1,8 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "INFO: Fetching libs from charmhub."
|
||||
charmcraft fetch-lib charms.data_platform_libs.v0.database_requires
|
||||
charmcraft fetch-lib charms.keystone_k8s.v1.identity_service
|
||||
charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq
|
||||
charmcraft fetch-lib charms.cinder_k8s.v0.storage_backend
|
||||
charmcraft fetch-lib charms.traefik_k8s.v2.ingress
|
@ -1,518 +0,0 @@
|
||||
"""IdentityServiceProvides and Requires module.
|
||||
|
||||
|
||||
This library contains the Requires and Provides classes for handling
|
||||
the identity_service interface.
|
||||
|
||||
Import `IdentityServiceRequires` in your charm, with the charm object and the
|
||||
relation name:
|
||||
- self
|
||||
- "identity_service"
|
||||
|
||||
Also provide additional parameters to the charm object:
|
||||
- service
|
||||
- internal_url
|
||||
- public_url
|
||||
- admin_url
|
||||
- region
|
||||
- username
|
||||
- vhost
|
||||
|
||||
Two events are also available to respond to:
|
||||
- connected
|
||||
- ready
|
||||
- goneaway
|
||||
|
||||
A basic example showing the usage of this relation follows:
|
||||
|
||||
```
|
||||
from charms.keystone_k8s.v1.identity_service import IdentityServiceRequires
|
||||
|
||||
class IdentityServiceClientCharm(CharmBase):
|
||||
def __init__(self, *args):
|
||||
super().__init__(*args)
|
||||
# IdentityService Requires
|
||||
self.identity_service = IdentityServiceRequires(
|
||||
self, "identity_service",
|
||||
service = "my-service"
|
||||
internal_url = "http://internal-url"
|
||||
public_url = "http://public-url"
|
||||
admin_url = "http://admin-url"
|
||||
region = "region"
|
||||
)
|
||||
self.framework.observe(
|
||||
self.identity_service.on.connected, self._on_identity_service_connected)
|
||||
self.framework.observe(
|
||||
self.identity_service.on.ready, self._on_identity_service_ready)
|
||||
self.framework.observe(
|
||||
self.identity_service.on.goneaway, self._on_identity_service_goneaway)
|
||||
|
||||
def _on_identity_service_connected(self, event):
|
||||
'''React to the IdentityService connected event.
|
||||
|
||||
This event happens when n IdentityService relation is added to the
|
||||
model before credentials etc have been provided.
|
||||
'''
|
||||
# Do something before the relation is complete
|
||||
pass
|
||||
|
||||
def _on_identity_service_ready(self, event):
|
||||
'''React to the IdentityService ready event.
|
||||
|
||||
The IdentityService interface will use the provided config for the
|
||||
request to the identity server.
|
||||
'''
|
||||
# IdentityService Relation is ready. Do something with the completed relation.
|
||||
pass
|
||||
|
||||
def _on_identity_service_goneaway(self, event):
|
||||
'''React to the IdentityService goneaway event.
|
||||
|
||||
This event happens when an IdentityService relation is removed.
|
||||
'''
|
||||
# IdentityService Relation has goneaway. shutdown services or suchlike
|
||||
pass
|
||||
```
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
from ops.framework import (
|
||||
StoredState,
|
||||
EventBase,
|
||||
ObjectEvents,
|
||||
EventSource,
|
||||
Object,
|
||||
)
|
||||
from ops.model import (
|
||||
Relation,
|
||||
SecretNotFoundError,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# The unique Charmhub library identifier, never change it
|
||||
LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0"
|
||||
|
||||
# Increment this major API version when introducing breaking changes
|
||||
LIBAPI = 1
|
||||
|
||||
# Increment this PATCH version before using `charmcraft publish-lib` or reset
|
||||
# to 0 if you are raising the major API version
|
||||
LIBPATCH = 0
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IdentityServiceConnectedEvent(EventBase):
|
||||
"""IdentityService connected Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class IdentityServiceReadyEvent(EventBase):
|
||||
"""IdentityService ready for use Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class IdentityServiceGoneAwayEvent(EventBase):
|
||||
"""IdentityService relation has gone-away Event"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class IdentityServiceServerEvents(ObjectEvents):
|
||||
"""Events class for `on`"""
|
||||
|
||||
connected = EventSource(IdentityServiceConnectedEvent)
|
||||
ready = EventSource(IdentityServiceReadyEvent)
|
||||
goneaway = EventSource(IdentityServiceGoneAwayEvent)
|
||||
|
||||
|
||||
class IdentityServiceRequires(Object):
|
||||
"""
|
||||
IdentityServiceRequires class
|
||||
"""
|
||||
|
||||
on = IdentityServiceServerEvents()
|
||||
_stored = StoredState()
|
||||
|
||||
def __init__(self, charm, relation_name: str, service_endpoints: dict,
|
||||
region: str):
|
||||
super().__init__(charm, relation_name)
|
||||
self.charm = charm
|
||||
self.relation_name = relation_name
|
||||
self.service_endpoints = service_endpoints
|
||||
self.region = region
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_joined,
|
||||
self._on_identity_service_relation_joined,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_changed,
|
||||
self._on_identity_service_relation_changed,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_departed,
|
||||
self._on_identity_service_relation_changed,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_broken,
|
||||
self._on_identity_service_relation_broken,
|
||||
)
|
||||
|
||||
def _on_identity_service_relation_joined(self, event):
|
||||
"""IdentityService relation joined."""
|
||||
logging.debug("IdentityService on_joined")
|
||||
self.on.connected.emit()
|
||||
self.register_services(
|
||||
self.service_endpoints,
|
||||
self.region)
|
||||
|
||||
def _on_identity_service_relation_changed(self, event):
|
||||
"""IdentityService relation changed."""
|
||||
logging.debug("IdentityService on_changed")
|
||||
try:
|
||||
self.service_password
|
||||
self.on.ready.emit()
|
||||
except (AttributeError, KeyError):
|
||||
pass
|
||||
|
||||
def _on_identity_service_relation_broken(self, event):
|
||||
"""IdentityService relation broken."""
|
||||
logging.debug("IdentityService on_broken")
|
||||
self.on.goneaway.emit()
|
||||
|
||||
@property
|
||||
def _identity_service_rel(self) -> Relation:
|
||||
"""The IdentityService relation."""
|
||||
return self.framework.model.get_relation(self.relation_name)
|
||||
|
||||
def get_remote_app_data(self, key: str) -> str:
|
||||
"""Return the value for the given key from remote app data."""
|
||||
data = self._identity_service_rel.data[self._identity_service_rel.app]
|
||||
return data.get(key)
|
||||
|
||||
@property
|
||||
def api_version(self) -> str:
|
||||
"""Return the api_version."""
|
||||
return self.get_remote_app_data('api-version')
|
||||
|
||||
@property
|
||||
def auth_host(self) -> str:
|
||||
"""Return the auth_host."""
|
||||
return self.get_remote_app_data('auth-host')
|
||||
|
||||
@property
|
||||
def auth_port(self) -> str:
|
||||
"""Return the auth_port."""
|
||||
return self.get_remote_app_data('auth-port')
|
||||
|
||||
@property
|
||||
def auth_protocol(self) -> str:
|
||||
"""Return the auth_protocol."""
|
||||
return self.get_remote_app_data('auth-protocol')
|
||||
|
||||
@property
|
||||
def internal_host(self) -> str:
|
||||
"""Return the internal_host."""
|
||||
return self.get_remote_app_data('internal-host')
|
||||
|
||||
@property
|
||||
def internal_port(self) -> str:
|
||||
"""Return the internal_port."""
|
||||
return self.get_remote_app_data('internal-port')
|
||||
|
||||
@property
|
||||
def internal_protocol(self) -> str:
|
||||
"""Return the internal_protocol."""
|
||||
return self.get_remote_app_data('internal-protocol')
|
||||
|
||||
@property
|
||||
def admin_domain_name(self) -> str:
|
||||
"""Return the admin_domain_name."""
|
||||
return self.get_remote_app_data('admin-domain-name')
|
||||
|
||||
@property
|
||||
def admin_domain_id(self) -> str:
|
||||
"""Return the admin_domain_id."""
|
||||
return self.get_remote_app_data('admin-domain-id')
|
||||
|
||||
@property
|
||||
def admin_project_name(self) -> str:
|
||||
"""Return the admin_project_name."""
|
||||
return self.get_remote_app_data('admin-project-name')
|
||||
|
||||
@property
|
||||
def admin_project_id(self) -> str:
|
||||
"""Return the admin_project_id."""
|
||||
return self.get_remote_app_data('admin-project-id')
|
||||
|
||||
@property
|
||||
def admin_user_name(self) -> str:
|
||||
"""Return the admin_user_name."""
|
||||
return self.get_remote_app_data('admin-user-name')
|
||||
|
||||
@property
|
||||
def admin_user_id(self) -> str:
|
||||
"""Return the admin_user_id."""
|
||||
return self.get_remote_app_data('admin-user-id')
|
||||
|
||||
@property
|
||||
def service_domain_name(self) -> str:
|
||||
"""Return the service_domain_name."""
|
||||
return self.get_remote_app_data('service-domain-name')
|
||||
|
||||
@property
|
||||
def service_domain_id(self) -> str:
|
||||
"""Return the service_domain_id."""
|
||||
return self.get_remote_app_data('service-domain-id')
|
||||
|
||||
@property
|
||||
def service_host(self) -> str:
|
||||
"""Return the service_host."""
|
||||
return self.get_remote_app_data('service-host')
|
||||
|
||||
@property
|
||||
def service_credentials(self) -> str:
|
||||
"""Return the service_credentials secret."""
|
||||
return self.get_remote_app_data('service-credentials')
|
||||
|
||||
@property
|
||||
def service_password(self) -> str:
|
||||
"""Return the service_password."""
|
||||
credentials_id = self.get_remote_app_data('service-credentials')
|
||||
if not credentials_id:
|
||||
return None
|
||||
|
||||
try:
|
||||
credentials = self.charm.model.get_secret(id=credentials_id)
|
||||
return credentials.get_content().get("password")
|
||||
except SecretNotFoundError:
|
||||
logger.warning(f"Secret {credentials_id} not found")
|
||||
return None
|
||||
|
||||
@property
|
||||
def service_port(self) -> str:
|
||||
"""Return the service_port."""
|
||||
return self.get_remote_app_data('service-port')
|
||||
|
||||
@property
|
||||
def service_protocol(self) -> str:
|
||||
"""Return the service_protocol."""
|
||||
return self.get_remote_app_data('service-protocol')
|
||||
|
||||
@property
|
||||
def service_project_name(self) -> str:
|
||||
"""Return the service_project_name."""
|
||||
return self.get_remote_app_data('service-project-name')
|
||||
|
||||
@property
|
||||
def service_project_id(self) -> str:
|
||||
"""Return the service_project_id."""
|
||||
return self.get_remote_app_data('service-project-id')
|
||||
|
||||
@property
|
||||
def service_user_name(self) -> str:
|
||||
"""Return the service_user_name."""
|
||||
credentials_id = self.get_remote_app_data('service-credentials')
|
||||
if not credentials_id:
|
||||
return None
|
||||
|
||||
try:
|
||||
credentials = self.charm.model.get_secret(id=credentials_id)
|
||||
return credentials.get_content().get("username")
|
||||
except SecretNotFoundError:
|
||||
logger.warning(f"Secret {credentials_id} not found")
|
||||
return None
|
||||
|
||||
@property
|
||||
def service_user_id(self) -> str:
|
||||
"""Return the service_user_id."""
|
||||
return self.get_remote_app_data('service-user-id')
|
||||
|
||||
@property
|
||||
def internal_auth_url(self) -> str:
|
||||
"""Return the internal_auth_url."""
|
||||
return self.get_remote_app_data('internal-auth-url')
|
||||
|
||||
@property
|
||||
def admin_auth_url(self) -> str:
|
||||
"""Return the admin_auth_url."""
|
||||
return self.get_remote_app_data('admin-auth-url')
|
||||
|
||||
@property
|
||||
def public_auth_url(self) -> str:
|
||||
"""Return the public_auth_url."""
|
||||
return self.get_remote_app_data('public-auth-url')
|
||||
|
||||
def register_services(self, service_endpoints: dict,
|
||||
region: str) -> None:
|
||||
"""Request access to the IdentityService server."""
|
||||
if self.model.unit.is_leader():
|
||||
logging.debug("Requesting service registration")
|
||||
app_data = self._identity_service_rel.data[self.charm.app]
|
||||
app_data["service-endpoints"] = json.dumps(
|
||||
service_endpoints, sort_keys=True
|
||||
)
|
||||
app_data["region"] = region
|
||||
|
||||
|
||||
class HasIdentityServiceClientsEvent(EventBase):
|
||||
"""Has IdentityServiceClients Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ReadyIdentityServiceClientsEvent(EventBase):
|
||||
"""IdentityServiceClients Ready Event."""
|
||||
|
||||
def __init__(self, handle, relation_id, relation_name, service_endpoints,
|
||||
region, client_app_name):
|
||||
super().__init__(handle)
|
||||
self.relation_id = relation_id
|
||||
self.relation_name = relation_name
|
||||
self.service_endpoints = service_endpoints
|
||||
self.region = region
|
||||
self.client_app_name = client_app_name
|
||||
|
||||
def snapshot(self):
|
||||
return {
|
||||
"relation_id": self.relation_id,
|
||||
"relation_name": self.relation_name,
|
||||
"service_endpoints": self.service_endpoints,
|
||||
"client_app_name": self.client_app_name,
|
||||
"region": self.region}
|
||||
|
||||
def restore(self, snapshot):
|
||||
super().restore(snapshot)
|
||||
self.relation_id = snapshot["relation_id"]
|
||||
self.relation_name = snapshot["relation_name"]
|
||||
self.service_endpoints = snapshot["service_endpoints"]
|
||||
self.region = snapshot["region"]
|
||||
self.client_app_name = snapshot["client_app_name"]
|
||||
|
||||
|
||||
class IdentityServiceClientEvents(ObjectEvents):
|
||||
"""Events class for `on`"""
|
||||
|
||||
has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent)
|
||||
ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent)
|
||||
|
||||
|
||||
class IdentityServiceProvides(Object):
|
||||
"""
|
||||
IdentityServiceProvides class
|
||||
"""
|
||||
|
||||
on = IdentityServiceClientEvents()
|
||||
_stored = StoredState()
|
||||
|
||||
def __init__(self, charm, relation_name):
|
||||
super().__init__(charm, relation_name)
|
||||
self.charm = charm
|
||||
self.relation_name = relation_name
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_joined,
|
||||
self._on_identity_service_relation_joined,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_changed,
|
||||
self._on_identity_service_relation_changed,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_broken,
|
||||
self._on_identity_service_relation_broken,
|
||||
)
|
||||
|
||||
def _on_identity_service_relation_joined(self, event):
|
||||
"""Handle IdentityService joined."""
|
||||
logging.debug("IdentityService on_joined")
|
||||
self.on.has_identity_service_clients.emit()
|
||||
|
||||
def _on_identity_service_relation_changed(self, event):
|
||||
"""Handle IdentityService changed."""
|
||||
logging.debug("IdentityService on_changed")
|
||||
REQUIRED_KEYS = [
|
||||
'service-endpoints',
|
||||
'region']
|
||||
|
||||
values = [
|
||||
event.relation.data[event.relation.app].get(k)
|
||||
for k in REQUIRED_KEYS
|
||||
]
|
||||
# Validate data on the relation
|
||||
if all(values):
|
||||
service_eps = json.loads(
|
||||
event.relation.data[event.relation.app]['service-endpoints'])
|
||||
self.on.ready_identity_service_clients.emit(
|
||||
event.relation.id,
|
||||
event.relation.name,
|
||||
service_eps,
|
||||
event.relation.data[event.relation.app]['region'],
|
||||
event.relation.app.name)
|
||||
|
||||
def _on_identity_service_relation_broken(self, event):
|
||||
"""Handle IdentityService broken."""
|
||||
logging.debug("IdentityServiceProvides on_departed")
|
||||
# TODO clear data on the relation
|
||||
|
||||
def set_identity_service_credentials(self, relation_name: int,
|
||||
relation_id: str,
|
||||
api_version: str,
|
||||
auth_host: str,
|
||||
auth_port: str,
|
||||
auth_protocol: str,
|
||||
internal_host: str,
|
||||
internal_port: str,
|
||||
internal_protocol: str,
|
||||
service_host: str,
|
||||
service_port: str,
|
||||
service_protocol: str,
|
||||
admin_domain: str,
|
||||
admin_project: str,
|
||||
admin_user: str,
|
||||
service_domain: str,
|
||||
service_project: str,
|
||||
service_user: str,
|
||||
internal_auth_url: str,
|
||||
admin_auth_url: str,
|
||||
public_auth_url: str,
|
||||
service_credentials: str):
|
||||
logging.debug("Setting identity_service connection information.")
|
||||
_identity_service_rel = None
|
||||
for relation in self.framework.model.relations[relation_name]:
|
||||
if relation.id == relation_id:
|
||||
_identity_service_rel = relation
|
||||
if not _identity_service_rel:
|
||||
# Relation has disappeared so skip send of data
|
||||
return
|
||||
app_data = _identity_service_rel.data[self.charm.app]
|
||||
app_data["api-version"] = api_version
|
||||
app_data["auth-host"] = auth_host
|
||||
app_data["auth-port"] = str(auth_port)
|
||||
app_data["auth-protocol"] = auth_protocol
|
||||
app_data["internal-host"] = internal_host
|
||||
app_data["internal-port"] = str(internal_port)
|
||||
app_data["internal-protocol"] = internal_protocol
|
||||
app_data["service-host"] = service_host
|
||||
app_data["service-port"] = str(service_port)
|
||||
app_data["service-protocol"] = service_protocol
|
||||
app_data["admin-domain-name"] = admin_domain.name
|
||||
app_data["admin-domain-id"] = admin_domain.id
|
||||
app_data["admin-project-name"] = admin_project.name
|
||||
app_data["admin-project-id"] = admin_project.id
|
||||
app_data["admin-user-name"] = admin_user.name
|
||||
app_data["admin-user-id"] = admin_user.id
|
||||
app_data["service-domain-name"] = service_domain.name
|
||||
app_data["service-domain-id"] = service_domain.id
|
||||
app_data["service-project-name"] = service_project.name
|
||||
app_data["service-project-id"] = service_project.id
|
||||
app_data["service-user-id"] = service_user.id
|
||||
app_data["internal-auth-url"] = internal_auth_url
|
||||
app_data["admin-auth-url"] = admin_auth_url
|
||||
app_data["public-auth-url"] = public_auth_url
|
||||
app_data["service-credentials"] = service_credentials
|
@ -1,286 +0,0 @@
|
||||
"""RabbitMQProvides and Requires module.
|
||||
|
||||
This library contains the Requires and Provides classes for handling
|
||||
the rabbitmq interface.
|
||||
|
||||
Import `RabbitMQRequires` in your charm, with the charm object and the
|
||||
relation name:
|
||||
- self
|
||||
- "amqp"
|
||||
|
||||
Also provide two additional parameters to the charm object:
|
||||
- username
|
||||
- vhost
|
||||
|
||||
Two events are also available to respond to:
|
||||
- connected
|
||||
- ready
|
||||
- goneaway
|
||||
|
||||
A basic example showing the usage of this relation follows:
|
||||
|
||||
```
|
||||
from charms.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires
|
||||
|
||||
class RabbitMQClientCharm(CharmBase):
|
||||
def __init__(self, *args):
|
||||
super().__init__(*args)
|
||||
# RabbitMQ Requires
|
||||
self.amqp = RabbitMQRequires(
|
||||
self, "amqp",
|
||||
username="myusername",
|
||||
vhost="vhostname"
|
||||
)
|
||||
self.framework.observe(
|
||||
self.amqp.on.connected, self._on_amqp_connected)
|
||||
self.framework.observe(
|
||||
self.amqp.on.ready, self._on_amqp_ready)
|
||||
self.framework.observe(
|
||||
self.amqp.on.goneaway, self._on_amqp_goneaway)
|
||||
|
||||
def _on_amqp_connected(self, event):
|
||||
'''React to the RabbitMQ connected event.
|
||||
|
||||
This event happens when n RabbitMQ relation is added to the
|
||||
model before credentials etc have been provided.
|
||||
'''
|
||||
# Do something before the relation is complete
|
||||
pass
|
||||
|
||||
def _on_amqp_ready(self, event):
|
||||
'''React to the RabbitMQ ready event.
|
||||
|
||||
The RabbitMQ interface will use the provided username and vhost for the
|
||||
request to the rabbitmq server.
|
||||
'''
|
||||
# RabbitMQ Relation is ready. Do something with the completed relation.
|
||||
pass
|
||||
|
||||
def _on_amqp_goneaway(self, event):
|
||||
'''React to the RabbitMQ goneaway event.
|
||||
|
||||
This event happens when an RabbitMQ relation is removed.
|
||||
'''
|
||||
# RabbitMQ Relation has goneaway. shutdown services or suchlike
|
||||
pass
|
||||
```
|
||||
"""
|
||||
|
||||
# The unique Charmhub library identifier, never change it
|
||||
LIBID = "45622352791142fd9cf87232e3bd6f2a"
|
||||
|
||||
# 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
|
||||
|
||||
import logging
|
||||
|
||||
from ops.framework import (
|
||||
StoredState,
|
||||
EventBase,
|
||||
ObjectEvents,
|
||||
EventSource,
|
||||
Object,
|
||||
)
|
||||
|
||||
from ops.model import Relation
|
||||
|
||||
from typing import List
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RabbitMQConnectedEvent(EventBase):
|
||||
"""RabbitMQ connected Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class RabbitMQReadyEvent(EventBase):
|
||||
"""RabbitMQ ready for use Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class RabbitMQGoneAwayEvent(EventBase):
|
||||
"""RabbitMQ relation has gone-away Event"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class RabbitMQServerEvents(ObjectEvents):
|
||||
"""Events class for `on`"""
|
||||
|
||||
connected = EventSource(RabbitMQConnectedEvent)
|
||||
ready = EventSource(RabbitMQReadyEvent)
|
||||
goneaway = EventSource(RabbitMQGoneAwayEvent)
|
||||
|
||||
|
||||
class RabbitMQRequires(Object):
|
||||
"""
|
||||
RabbitMQRequires class
|
||||
"""
|
||||
|
||||
on = RabbitMQServerEvents()
|
||||
|
||||
def __init__(self, charm, relation_name: str, username: str, vhost: str):
|
||||
super().__init__(charm, relation_name)
|
||||
self.charm = charm
|
||||
self.relation_name = relation_name
|
||||
self.username = username
|
||||
self.vhost = vhost
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_joined,
|
||||
self._on_amqp_relation_joined,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_changed,
|
||||
self._on_amqp_relation_changed,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_departed,
|
||||
self._on_amqp_relation_changed,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_broken,
|
||||
self._on_amqp_relation_broken,
|
||||
)
|
||||
|
||||
def _on_amqp_relation_joined(self, event):
|
||||
"""RabbitMQ relation joined."""
|
||||
logging.debug("RabbitMQRabbitMQRequires on_joined")
|
||||
self.on.connected.emit()
|
||||
self.request_access(self.username, self.vhost)
|
||||
|
||||
def _on_amqp_relation_changed(self, event):
|
||||
"""RabbitMQ relation changed."""
|
||||
logging.debug("RabbitMQRabbitMQRequires on_changed/departed")
|
||||
if self.password:
|
||||
self.on.ready.emit()
|
||||
|
||||
def _on_amqp_relation_broken(self, event):
|
||||
"""RabbitMQ relation broken."""
|
||||
logging.debug("RabbitMQRabbitMQRequires on_broken")
|
||||
self.on.goneaway.emit()
|
||||
|
||||
@property
|
||||
def _amqp_rel(self) -> Relation:
|
||||
"""The RabbitMQ relation."""
|
||||
return self.framework.model.get_relation(self.relation_name)
|
||||
|
||||
@property
|
||||
def password(self) -> str:
|
||||
"""Return the RabbitMQ password from the server side of the relation."""
|
||||
return self._amqp_rel.data[self._amqp_rel.app].get("password")
|
||||
|
||||
@property
|
||||
def hostname(self) -> str:
|
||||
"""Return the hostname from the RabbitMQ relation"""
|
||||
return self._amqp_rel.data[self._amqp_rel.app].get("hostname")
|
||||
|
||||
@property
|
||||
def ssl_port(self) -> str:
|
||||
"""Return the SSL port from the RabbitMQ relation"""
|
||||
return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port")
|
||||
|
||||
@property
|
||||
def ssl_ca(self) -> str:
|
||||
"""Return the SSL port from the RabbitMQ relation"""
|
||||
return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca")
|
||||
|
||||
@property
|
||||
def hostnames(self) -> List[str]:
|
||||
"""Return a list of remote RMQ hosts from the RabbitMQ relation"""
|
||||
_hosts = []
|
||||
for unit in self._amqp_rel.units:
|
||||
_hosts.append(self._amqp_rel.data[unit].get("ingress-address"))
|
||||
return _hosts
|
||||
|
||||
def request_access(self, username: str, vhost: str) -> None:
|
||||
"""Request access to the RabbitMQ server."""
|
||||
if self.model.unit.is_leader():
|
||||
logging.debug("Requesting RabbitMQ user and vhost")
|
||||
self._amqp_rel.data[self.charm.app]["username"] = username
|
||||
self._amqp_rel.data[self.charm.app]["vhost"] = vhost
|
||||
|
||||
|
||||
class HasRabbitMQClientsEvent(EventBase):
|
||||
"""Has RabbitMQClients Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ReadyRabbitMQClientsEvent(EventBase):
|
||||
"""RabbitMQClients Ready Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class RabbitMQClientEvents(ObjectEvents):
|
||||
"""Events class for `on`"""
|
||||
|
||||
has_amqp_clients = EventSource(HasRabbitMQClientsEvent)
|
||||
ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent)
|
||||
|
||||
|
||||
class RabbitMQProvides(Object):
|
||||
"""
|
||||
RabbitMQProvides class
|
||||
"""
|
||||
|
||||
on = RabbitMQClientEvents()
|
||||
|
||||
def __init__(self, charm, relation_name, callback):
|
||||
super().__init__(charm, relation_name)
|
||||
self.charm = charm
|
||||
self.relation_name = relation_name
|
||||
self.callback = callback
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_joined,
|
||||
self._on_amqp_relation_joined,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_changed,
|
||||
self._on_amqp_relation_changed,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_broken,
|
||||
self._on_amqp_relation_broken,
|
||||
)
|
||||
|
||||
def _on_amqp_relation_joined(self, event):
|
||||
"""Handle RabbitMQ joined."""
|
||||
logging.debug("RabbitMQRabbitMQProvides on_joined data={}"
|
||||
.format(event.relation.data[event.relation.app]))
|
||||
self.on.has_amqp_clients.emit()
|
||||
|
||||
def _on_amqp_relation_changed(self, event):
|
||||
"""Handle RabbitMQ changed."""
|
||||
logging.debug("RabbitMQRabbitMQProvides on_changed data={}"
|
||||
.format(event.relation.data[event.relation.app]))
|
||||
# Validate data on the relation
|
||||
if self.username(event) and self.vhost(event):
|
||||
self.on.ready_amqp_clients.emit()
|
||||
if self.charm.unit.is_leader():
|
||||
self.callback(event, self.username(event), self.vhost(event))
|
||||
else:
|
||||
logging.warning("Received RabbitMQ changed event without the "
|
||||
"expected keys ('username', 'vhost') in the "
|
||||
"application data bag. Incompatible charm in "
|
||||
"other end of relation?")
|
||||
|
||||
def _on_amqp_relation_broken(self, event):
|
||||
"""Handle RabbitMQ broken."""
|
||||
logging.debug("RabbitMQRabbitMQProvides on_departed")
|
||||
# TODO clear data on the relation
|
||||
|
||||
def username(self, event):
|
||||
"""Return the RabbitMQ username from the client side of the relation."""
|
||||
return event.relation.data[event.relation.app].get("username")
|
||||
|
||||
def vhost(self, event):
|
||||
"""Return the RabbitMQ vhost from the client side of the relation."""
|
||||
return event.relation.data[event.relation.app].get("vhost")
|
@ -1,734 +0,0 @@
|
||||
# Copyright 2023 Canonical Ltd.
|
||||
# See LICENSE file for licensing details.
|
||||
|
||||
r"""# Interface Library for ingress.
|
||||
|
||||
This library wraps relation endpoints using the `ingress` interface
|
||||
and provides a Python API for both requesting and providing per-application
|
||||
ingress, with load-balancing occurring across all units.
|
||||
|
||||
## Getting Started
|
||||
|
||||
To get started using the library, you just need to fetch the library using `charmcraft`.
|
||||
|
||||
```shell
|
||||
cd some-charm
|
||||
charmcraft fetch-lib charms.traefik_k8s.v1.ingress
|
||||
```
|
||||
|
||||
In the `metadata.yaml` of the charm, add the following:
|
||||
|
||||
```yaml
|
||||
requires:
|
||||
ingress:
|
||||
interface: ingress
|
||||
limit: 1
|
||||
```
|
||||
|
||||
Then, to initialise the library:
|
||||
|
||||
```python
|
||||
from charms.traefik_k8s.v2.ingress import (IngressPerAppRequirer,
|
||||
IngressPerAppReadyEvent, IngressPerAppRevokedEvent)
|
||||
|
||||
class SomeCharm(CharmBase):
|
||||
def __init__(self, *args):
|
||||
# ...
|
||||
self.ingress = IngressPerAppRequirer(self, port=80)
|
||||
# The following event is triggered when the ingress URL to be used
|
||||
# by this deployment of the `SomeCharm` is ready (or changes).
|
||||
self.framework.observe(
|
||||
self.ingress.on.ready, self._on_ingress_ready
|
||||
)
|
||||
self.framework.observe(
|
||||
self.ingress.on.revoked, self._on_ingress_revoked
|
||||
)
|
||||
|
||||
def _on_ingress_ready(self, event: IngressPerAppReadyEvent):
|
||||
logger.info("This app's ingress URL: %s", event.url)
|
||||
|
||||
def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent):
|
||||
logger.info("This app no longer has ingress")
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import socket
|
||||
import typing
|
||||
from dataclasses import dataclass
|
||||
from typing import (
|
||||
Any,
|
||||
Dict,
|
||||
List,
|
||||
MutableMapping,
|
||||
Optional,
|
||||
Sequence,
|
||||
Tuple,
|
||||
)
|
||||
|
||||
import pydantic
|
||||
from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent
|
||||
from ops.framework import EventSource, Object, ObjectEvents, StoredState
|
||||
from ops.model import ModelError, Relation, Unit
|
||||
from pydantic import AnyHttpUrl, BaseModel, Field, validator
|
||||
|
||||
# The unique Charmhub library identifier, never change it
|
||||
LIBID = "e6de2a5cd5b34422a204668f3b8f90d2"
|
||||
|
||||
# Increment this major API version when introducing breaking changes
|
||||
LIBAPI = 2
|
||||
|
||||
# Increment this PATCH version before using `charmcraft publish-lib` or reset
|
||||
# to 0 if you are raising the major API version
|
||||
LIBPATCH = 6
|
||||
|
||||
PYDEPS = ["pydantic<2.0"]
|
||||
|
||||
DEFAULT_RELATION_NAME = "ingress"
|
||||
RELATION_INTERFACE = "ingress"
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"}
|
||||
|
||||
|
||||
class DatabagModel(BaseModel):
|
||||
"""Base databag model."""
|
||||
|
||||
class Config:
|
||||
"""Pydantic config."""
|
||||
|
||||
allow_population_by_field_name = True
|
||||
"""Allow instantiating this class by field name (instead of forcing alias)."""
|
||||
|
||||
_NEST_UNDER = None
|
||||
|
||||
@classmethod
|
||||
def load(cls, databag: MutableMapping):
|
||||
"""Load this model from a Juju databag."""
|
||||
if cls._NEST_UNDER:
|
||||
return cls.parse_obj(json.loads(databag[cls._NEST_UNDER]))
|
||||
|
||||
try:
|
||||
data = {k: json.loads(v) for k, v in databag.items() if k not in BUILTIN_JUJU_KEYS}
|
||||
except json.JSONDecodeError as e:
|
||||
msg = f"invalid databag contents: expecting json. {databag}"
|
||||
log.error(msg)
|
||||
raise DataValidationError(msg) from e
|
||||
|
||||
try:
|
||||
return cls.parse_raw(json.dumps(data)) # type: ignore
|
||||
except pydantic.ValidationError as e:
|
||||
msg = f"failed to validate databag: {databag}"
|
||||
log.error(msg, exc_info=True)
|
||||
raise DataValidationError(msg) from e
|
||||
|
||||
def dump(self, databag: Optional[MutableMapping] = None, clear: bool = True):
|
||||
"""Write the contents of this model to Juju databag.
|
||||
|
||||
:param databag: the databag to write the data to.
|
||||
:param clear: ensure the databag is cleared before writing it.
|
||||
"""
|
||||
if clear and databag:
|
||||
databag.clear()
|
||||
|
||||
if databag is None:
|
||||
databag = {}
|
||||
|
||||
if self._NEST_UNDER:
|
||||
databag[self._NEST_UNDER] = self.json()
|
||||
|
||||
dct = self.dict()
|
||||
for key, field in self.__fields__.items(): # type: ignore
|
||||
value = dct[key]
|
||||
databag[field.alias or key] = json.dumps(value)
|
||||
|
||||
return databag
|
||||
|
||||
|
||||
# todo: import these models from charm-relation-interfaces/ingress/v2 instead of redeclaring them
|
||||
class IngressUrl(BaseModel):
|
||||
"""Ingress url schema."""
|
||||
|
||||
url: AnyHttpUrl
|
||||
|
||||
|
||||
class IngressProviderAppData(DatabagModel):
|
||||
"""Ingress application databag schema."""
|
||||
|
||||
ingress: IngressUrl
|
||||
|
||||
|
||||
class ProviderSchema(BaseModel):
|
||||
"""Provider schema for Ingress."""
|
||||
|
||||
app: IngressProviderAppData
|
||||
|
||||
|
||||
class IngressRequirerAppData(DatabagModel):
|
||||
"""Ingress requirer application databag model."""
|
||||
|
||||
model: str = Field(description="The model the application is in.")
|
||||
name: str = Field(description="the name of the app requesting ingress.")
|
||||
port: int = Field(description="The port the app wishes to be exposed.")
|
||||
|
||||
# fields on top of vanilla 'ingress' interface:
|
||||
strip_prefix: Optional[bool] = Field(
|
||||
description="Whether to strip the prefix from the ingress url.", alias="strip-prefix"
|
||||
)
|
||||
redirect_https: Optional[bool] = Field(
|
||||
description="Whether to redirect http traffic to https.", alias="redirect-https"
|
||||
)
|
||||
|
||||
scheme: Optional[str] = Field(
|
||||
default="http", description="What scheme to use in the generated ingress url"
|
||||
)
|
||||
|
||||
@validator("scheme", pre=True)
|
||||
def validate_scheme(cls, scheme): # noqa: N805 # pydantic wants 'cls' as first arg
|
||||
"""Validate scheme arg."""
|
||||
if scheme not in {"http", "https", "h2c"}:
|
||||
raise ValueError("invalid scheme: should be one of `http|https|h2c`")
|
||||
return scheme
|
||||
|
||||
@validator("port", pre=True)
|
||||
def validate_port(cls, port): # noqa: N805 # pydantic wants 'cls' as first arg
|
||||
"""Validate port."""
|
||||
assert isinstance(port, int), type(port)
|
||||
assert 0 < port < 65535, "port out of TCP range"
|
||||
return port
|
||||
|
||||
|
||||
class IngressRequirerUnitData(DatabagModel):
|
||||
"""Ingress requirer unit databag model."""
|
||||
|
||||
host: str = Field(description="Hostname the unit wishes to be exposed.")
|
||||
|
||||
@validator("host", pre=True)
|
||||
def validate_host(cls, host): # noqa: N805 # pydantic wants 'cls' as first arg
|
||||
"""Validate host."""
|
||||
assert isinstance(host, str), type(host)
|
||||
return host
|
||||
|
||||
|
||||
class RequirerSchema(BaseModel):
|
||||
"""Requirer schema for Ingress."""
|
||||
|
||||
app: IngressRequirerAppData
|
||||
unit: IngressRequirerUnitData
|
||||
|
||||
|
||||
class IngressError(RuntimeError):
|
||||
"""Base class for custom errors raised by this library."""
|
||||
|
||||
|
||||
class NotReadyError(IngressError):
|
||||
"""Raised when a relation is not ready."""
|
||||
|
||||
|
||||
class DataValidationError(IngressError):
|
||||
"""Raised when data validation fails on IPU relation data."""
|
||||
|
||||
|
||||
class _IngressPerAppBase(Object):
|
||||
"""Base class for IngressPerUnit interface classes."""
|
||||
|
||||
def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME):
|
||||
super().__init__(charm, relation_name)
|
||||
|
||||
self.charm: CharmBase = charm
|
||||
self.relation_name = relation_name
|
||||
self.app = self.charm.app
|
||||
self.unit = self.charm.unit
|
||||
|
||||
observe = self.framework.observe
|
||||
rel_events = charm.on[relation_name]
|
||||
observe(rel_events.relation_created, self._handle_relation)
|
||||
observe(rel_events.relation_joined, self._handle_relation)
|
||||
observe(rel_events.relation_changed, self._handle_relation)
|
||||
observe(rel_events.relation_broken, self._handle_relation_broken)
|
||||
observe(charm.on.leader_elected, self._handle_upgrade_or_leader) # type: ignore
|
||||
observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader) # type: ignore
|
||||
|
||||
@property
|
||||
def relations(self):
|
||||
"""The list of Relation instances associated with this endpoint."""
|
||||
return list(self.charm.model.relations[self.relation_name])
|
||||
|
||||
def _handle_relation(self, event):
|
||||
"""Subclasses should implement this method to handle a relation update."""
|
||||
pass
|
||||
|
||||
def _handle_relation_broken(self, event):
|
||||
"""Subclasses should implement this method to handle a relation breaking."""
|
||||
pass
|
||||
|
||||
def _handle_upgrade_or_leader(self, event):
|
||||
"""Subclasses should implement this method to handle upgrades or leadership change."""
|
||||
pass
|
||||
|
||||
|
||||
class _IPAEvent(RelationEvent):
|
||||
__args__: Tuple[str, ...] = ()
|
||||
__optional_kwargs__: Dict[str, Any] = {}
|
||||
|
||||
@classmethod
|
||||
def __attrs__(cls):
|
||||
return cls.__args__ + tuple(cls.__optional_kwargs__.keys())
|
||||
|
||||
def __init__(self, handle, relation, *args, **kwargs):
|
||||
super().__init__(handle, relation)
|
||||
|
||||
if not len(self.__args__) == len(args):
|
||||
raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args)))
|
||||
|
||||
for attr, obj in zip(self.__args__, args):
|
||||
setattr(self, attr, obj)
|
||||
for attr, default in self.__optional_kwargs__.items():
|
||||
obj = kwargs.get(attr, default)
|
||||
setattr(self, attr, obj)
|
||||
|
||||
def snapshot(self):
|
||||
dct = super().snapshot()
|
||||
for attr in self.__attrs__():
|
||||
obj = getattr(self, attr)
|
||||
try:
|
||||
dct[attr] = obj
|
||||
except ValueError as e:
|
||||
raise ValueError(
|
||||
"cannot automagically serialize {}: "
|
||||
"override this method and do it "
|
||||
"manually.".format(obj)
|
||||
) from e
|
||||
|
||||
return dct
|
||||
|
||||
def restore(self, snapshot) -> None:
|
||||
super().restore(snapshot)
|
||||
for attr, obj in snapshot.items():
|
||||
setattr(self, attr, obj)
|
||||
|
||||
|
||||
class IngressPerAppDataProvidedEvent(_IPAEvent):
|
||||
"""Event representing that ingress data has been provided for an app."""
|
||||
|
||||
__args__ = ("name", "model", "hosts", "strip_prefix", "redirect_https")
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
name: Optional[str] = None
|
||||
model: Optional[str] = None
|
||||
# sequence of hostname, port dicts
|
||||
hosts: Sequence["IngressRequirerUnitData"] = ()
|
||||
strip_prefix: bool = False
|
||||
redirect_https: bool = False
|
||||
|
||||
|
||||
class IngressPerAppDataRemovedEvent(RelationEvent):
|
||||
"""Event representing that ingress data has been removed for an app."""
|
||||
|
||||
|
||||
class IngressPerAppProviderEvents(ObjectEvents):
|
||||
"""Container for IPA Provider events."""
|
||||
|
||||
data_provided = EventSource(IngressPerAppDataProvidedEvent)
|
||||
data_removed = EventSource(IngressPerAppDataRemovedEvent)
|
||||
|
||||
|
||||
@dataclass
|
||||
class IngressRequirerData:
|
||||
"""Data exposed by the ingress requirer to the provider."""
|
||||
|
||||
app: "IngressRequirerAppData"
|
||||
units: List["IngressRequirerUnitData"]
|
||||
|
||||
|
||||
class TlsProviderType(typing.Protocol):
|
||||
"""Placeholder."""
|
||||
|
||||
@property
|
||||
def enabled(self) -> bool: # type: ignore
|
||||
"""Placeholder."""
|
||||
|
||||
|
||||
class IngressPerAppProvider(_IngressPerAppBase):
|
||||
"""Implementation of the provider of ingress."""
|
||||
|
||||
on = IngressPerAppProviderEvents() # type: ignore
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
charm: CharmBase,
|
||||
relation_name: str = DEFAULT_RELATION_NAME,
|
||||
):
|
||||
"""Constructor for IngressPerAppProvider.
|
||||
|
||||
Args:
|
||||
charm: The charm that is instantiating the instance.
|
||||
relation_name: The name of the relation endpoint to bind to
|
||||
(defaults to "ingress").
|
||||
"""
|
||||
super().__init__(charm, relation_name)
|
||||
|
||||
def _handle_relation(self, event):
|
||||
# created, joined or changed: if remote side has sent the required data:
|
||||
# notify listeners.
|
||||
if self.is_ready(event.relation):
|
||||
data = self.get_data(event.relation)
|
||||
self.on.data_provided.emit( # type: ignore
|
||||
event.relation,
|
||||
data.app.name,
|
||||
data.app.model,
|
||||
[unit.dict() for unit in data.units],
|
||||
data.app.strip_prefix or False,
|
||||
data.app.redirect_https or False,
|
||||
)
|
||||
|
||||
def _handle_relation_broken(self, event):
|
||||
self.on.data_removed.emit(event.relation) # type: ignore
|
||||
|
||||
def wipe_ingress_data(self, relation: Relation):
|
||||
"""Clear ingress data from relation."""
|
||||
assert self.unit.is_leader(), "only leaders can do this"
|
||||
try:
|
||||
relation.data
|
||||
except ModelError as e:
|
||||
log.warning(
|
||||
"error {} accessing relation data for {!r}. "
|
||||
"Probably a ghost of a dead relation is still "
|
||||
"lingering around.".format(e, relation.name)
|
||||
)
|
||||
return
|
||||
del relation.data[self.app]["ingress"]
|
||||
|
||||
def _get_requirer_units_data(self, relation: Relation) -> List["IngressRequirerUnitData"]:
|
||||
"""Fetch and validate the requirer's app databag."""
|
||||
out: List["IngressRequirerUnitData"] = []
|
||||
|
||||
unit: Unit
|
||||
for unit in relation.units:
|
||||
databag = relation.data[unit]
|
||||
try:
|
||||
data = IngressRequirerUnitData.load(databag)
|
||||
out.append(data)
|
||||
except pydantic.ValidationError:
|
||||
log.info(f"failed to validate remote unit data for {unit}")
|
||||
raise
|
||||
return out
|
||||
|
||||
@staticmethod
|
||||
def _get_requirer_app_data(relation: Relation) -> "IngressRequirerAppData":
|
||||
"""Fetch and validate the requirer's app databag."""
|
||||
app = relation.app
|
||||
if app is None:
|
||||
raise NotReadyError(relation)
|
||||
|
||||
databag = relation.data[app]
|
||||
return IngressRequirerAppData.load(databag)
|
||||
|
||||
def get_data(self, relation: Relation) -> IngressRequirerData:
|
||||
"""Fetch the remote (requirer) app and units' databags."""
|
||||
try:
|
||||
return IngressRequirerData(
|
||||
self._get_requirer_app_data(relation), self._get_requirer_units_data(relation)
|
||||
)
|
||||
except (pydantic.ValidationError, DataValidationError) as e:
|
||||
raise DataValidationError("failed to validate ingress requirer data") from e
|
||||
|
||||
def is_ready(self, relation: Optional[Relation] = None):
|
||||
"""The Provider is ready if the requirer has sent valid data."""
|
||||
if not relation:
|
||||
return any(map(self.is_ready, self.relations))
|
||||
|
||||
try:
|
||||
self.get_data(relation)
|
||||
except (DataValidationError, NotReadyError) as e:
|
||||
log.debug("Provider not ready; validation error encountered: %s" % str(e))
|
||||
return False
|
||||
return True
|
||||
|
||||
def _published_url(self, relation: Relation) -> Optional["IngressProviderAppData"]:
|
||||
"""Fetch and validate this app databag; return the ingress url."""
|
||||
if not self.is_ready(relation) or not self.unit.is_leader():
|
||||
# Handle edge case where remote app name can be missing, e.g.,
|
||||
# relation_broken events.
|
||||
# Also, only leader units can read own app databags.
|
||||
# FIXME https://github.com/canonical/traefik-k8s-operator/issues/34
|
||||
return None
|
||||
|
||||
# fetch the provider's app databag
|
||||
databag = relation.data[self.app]
|
||||
if not databag.get("ingress"):
|
||||
raise NotReadyError("This application did not `publish_url` yet.")
|
||||
|
||||
return IngressProviderAppData.load(databag)
|
||||
|
||||
def publish_url(self, relation: Relation, url: str):
|
||||
"""Publish to the app databag the ingress url."""
|
||||
ingress_url = {"url": url}
|
||||
IngressProviderAppData.parse_obj({"ingress": ingress_url}).dump(relation.data[self.app])
|
||||
|
||||
@property
|
||||
def proxied_endpoints(self) -> Dict[str, str]:
|
||||
"""Returns the ingress settings provided to applications by this IngressPerAppProvider.
|
||||
|
||||
For example, when this IngressPerAppProvider has provided the
|
||||
`http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary
|
||||
will be:
|
||||
|
||||
```
|
||||
{
|
||||
"my-app": {
|
||||
"url": "http://foo.bar/my-model.my-app"
|
||||
}
|
||||
}
|
||||
```
|
||||
"""
|
||||
results = {}
|
||||
|
||||
for ingress_relation in self.relations:
|
||||
if not ingress_relation.app:
|
||||
log.warning(
|
||||
f"no app in relation {ingress_relation} when fetching proxied endpoints: skipping"
|
||||
)
|
||||
continue
|
||||
try:
|
||||
ingress_data = self._published_url(ingress_relation)
|
||||
except NotReadyError:
|
||||
log.warning(
|
||||
f"no published url found in {ingress_relation}: "
|
||||
f"traefik didn't publish_url yet to this relation."
|
||||
)
|
||||
continue
|
||||
|
||||
if not ingress_data:
|
||||
log.warning(f"relation {ingress_relation} not ready yet: try again in some time.")
|
||||
continue
|
||||
|
||||
results[ingress_relation.app.name] = ingress_data.ingress.dict()
|
||||
return results
|
||||
|
||||
|
||||
class IngressPerAppReadyEvent(_IPAEvent):
|
||||
"""Event representing that ingress for an app is ready."""
|
||||
|
||||
__args__ = ("url",)
|
||||
if typing.TYPE_CHECKING:
|
||||
url: Optional[str] = None
|
||||
|
||||
|
||||
class IngressPerAppRevokedEvent(RelationEvent):
|
||||
"""Event representing that ingress for an app has been revoked."""
|
||||
|
||||
|
||||
class IngressPerAppRequirerEvents(ObjectEvents):
|
||||
"""Container for IPA Requirer events."""
|
||||
|
||||
ready = EventSource(IngressPerAppReadyEvent)
|
||||
revoked = EventSource(IngressPerAppRevokedEvent)
|
||||
|
||||
|
||||
class IngressPerAppRequirer(_IngressPerAppBase):
|
||||
"""Implementation of the requirer of the ingress relation."""
|
||||
|
||||
on = IngressPerAppRequirerEvents() # type: ignore
|
||||
|
||||
# used to prevent spurious urls to be sent out if the event we're currently
|
||||
# handling is a relation-broken one.
|
||||
_stored = StoredState()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
charm: CharmBase,
|
||||
relation_name: str = DEFAULT_RELATION_NAME,
|
||||
*,
|
||||
host: Optional[str] = None,
|
||||
port: Optional[int] = None,
|
||||
strip_prefix: bool = False,
|
||||
redirect_https: bool = False,
|
||||
# fixme: this is horrible UX.
|
||||
# shall we switch to manually calling provide_ingress_requirements with all args when ready?
|
||||
scheme: typing.Callable[[], str] = lambda: "http",
|
||||
):
|
||||
"""Constructor for IngressRequirer.
|
||||
|
||||
The request args can be used to specify the ingress properties when the
|
||||
instance is created. If any are set, at least `port` is required, and
|
||||
they will be sent to the ingress provider as soon as it is available.
|
||||
All request args must be given as keyword args.
|
||||
|
||||
Args:
|
||||
charm: the charm that is instantiating the library.
|
||||
relation_name: the name of the relation endpoint to bind to (defaults to `ingress`);
|
||||
relation must be of interface type `ingress` and have "limit: 1")
|
||||
host: Hostname to be used by the ingress provider to address the requiring
|
||||
application; if unspecified, the default Kubernetes service name will be used.
|
||||
strip_prefix: configure Traefik to strip the path prefix.
|
||||
redirect_https: redirect incoming requests to HTTPS.
|
||||
scheme: callable returning the scheme to use when constructing the ingress url.
|
||||
|
||||
Request Args:
|
||||
port: the port of the service
|
||||
"""
|
||||
super().__init__(charm, relation_name)
|
||||
self.charm: CharmBase = charm
|
||||
self.relation_name = relation_name
|
||||
self._strip_prefix = strip_prefix
|
||||
self._redirect_https = redirect_https
|
||||
self._get_scheme = scheme
|
||||
|
||||
self._stored.set_default(current_url=None) # type: ignore
|
||||
|
||||
# if instantiated with a port, and we are related, then
|
||||
# we immediately publish our ingress data to speed up the process.
|
||||
if port:
|
||||
self._auto_data = host, port
|
||||
else:
|
||||
self._auto_data = None
|
||||
|
||||
def _handle_relation(self, event):
|
||||
# created, joined or changed: if we have auto data: publish it
|
||||
self._publish_auto_data()
|
||||
|
||||
if self.is_ready():
|
||||
# Avoid spurious events, emit only when there is a NEW URL available
|
||||
new_url = (
|
||||
None
|
||||
if isinstance(event, RelationBrokenEvent)
|
||||
else self._get_url_from_relation_data()
|
||||
)
|
||||
if self._stored.current_url != new_url: # type: ignore
|
||||
self._stored.current_url = new_url # type: ignore
|
||||
self.on.ready.emit(event.relation, new_url) # type: ignore
|
||||
|
||||
def _handle_relation_broken(self, event):
|
||||
self._stored.current_url = None # type: ignore
|
||||
self.on.revoked.emit(event.relation) # type: ignore
|
||||
|
||||
def _handle_upgrade_or_leader(self, event):
|
||||
"""On upgrade/leadership change: ensure we publish the data we have."""
|
||||
self._publish_auto_data()
|
||||
|
||||
def is_ready(self):
|
||||
"""The Requirer is ready if the Provider has sent valid data."""
|
||||
try:
|
||||
return bool(self._get_url_from_relation_data())
|
||||
except DataValidationError as e:
|
||||
log.debug("Requirer not ready; validation error encountered: %s" % str(e))
|
||||
return False
|
||||
|
||||
def _publish_auto_data(self):
|
||||
if self._auto_data:
|
||||
host, port = self._auto_data
|
||||
self.provide_ingress_requirements(host=host, port=port)
|
||||
|
||||
def provide_ingress_requirements(
|
||||
self,
|
||||
*,
|
||||
scheme: Optional[str] = None,
|
||||
host: Optional[str] = None,
|
||||
port: int,
|
||||
):
|
||||
"""Publishes the data that Traefik needs to provide ingress.
|
||||
|
||||
Args:
|
||||
scheme: Scheme to be used; if unspecified, use the one used by __init__.
|
||||
host: Hostname to be used by the ingress provider to address the
|
||||
requirer unit; if unspecified, FQDN will be used instead
|
||||
port: the port of the service (required)
|
||||
"""
|
||||
for relation in self.relations:
|
||||
self._provide_ingress_requirements(scheme, host, port, relation)
|
||||
|
||||
def _provide_ingress_requirements(
|
||||
self,
|
||||
scheme: Optional[str],
|
||||
host: Optional[str],
|
||||
port: int,
|
||||
relation: Relation,
|
||||
):
|
||||
if self.unit.is_leader():
|
||||
self._publish_app_data(scheme, port, relation)
|
||||
|
||||
self._publish_unit_data(host, relation)
|
||||
|
||||
def _publish_unit_data(
|
||||
self,
|
||||
host: Optional[str],
|
||||
relation: Relation,
|
||||
):
|
||||
if not host:
|
||||
host = socket.getfqdn()
|
||||
|
||||
unit_databag = relation.data[self.unit]
|
||||
try:
|
||||
IngressRequirerUnitData(host=host).dump(unit_databag)
|
||||
except pydantic.ValidationError as e:
|
||||
msg = "failed to validate unit data"
|
||||
log.info(msg, exc_info=True) # log to INFO because this might be expected
|
||||
raise DataValidationError(msg) from e
|
||||
|
||||
def _publish_app_data(
|
||||
self,
|
||||
scheme: Optional[str],
|
||||
port: int,
|
||||
relation: Relation,
|
||||
):
|
||||
# assumes leadership!
|
||||
app_databag = relation.data[self.app]
|
||||
|
||||
if not scheme:
|
||||
# If scheme was not provided, use the one given to the constructor.
|
||||
scheme = self._get_scheme()
|
||||
|
||||
try:
|
||||
IngressRequirerAppData( # type: ignore # pyright does not like aliases
|
||||
model=self.model.name,
|
||||
name=self.app.name,
|
||||
scheme=scheme,
|
||||
port=port,
|
||||
strip_prefix=self._strip_prefix, # type: ignore # pyright does not like aliases
|
||||
redirect_https=self._redirect_https, # type: ignore # pyright does not like aliases
|
||||
).dump(app_databag)
|
||||
except pydantic.ValidationError as e:
|
||||
msg = "failed to validate app data"
|
||||
log.info(msg, exc_info=True) # log to INFO because this might be expected
|
||||
raise DataValidationError(msg) from e
|
||||
|
||||
@property
|
||||
def relation(self):
|
||||
"""The established Relation instance, or None."""
|
||||
return self.relations[0] if self.relations else None
|
||||
|
||||
def _get_url_from_relation_data(self) -> Optional[str]:
|
||||
"""The full ingress URL to reach the current unit.
|
||||
|
||||
Returns None if the URL isn't available yet.
|
||||
"""
|
||||
relation = self.relation
|
||||
if not relation or not relation.app:
|
||||
return None
|
||||
|
||||
# fetch the provider's app databag
|
||||
try:
|
||||
databag = relation.data[relation.app]
|
||||
except ModelError as e:
|
||||
log.debug(
|
||||
f"Error {e} attempting to read remote app data; "
|
||||
f"probably we are in a relation_departed hook"
|
||||
)
|
||||
return None
|
||||
|
||||
if not databag: # not ready yet
|
||||
return None
|
||||
|
||||
return str(IngressProviderAppData.load(databag).ingress.url)
|
||||
|
||||
@property
|
||||
def url(self) -> Optional[str]:
|
||||
"""The full ingress URL to reach the current unit.
|
||||
|
||||
Returns None if the URL isn't available yet.
|
||||
"""
|
||||
data = (
|
||||
typing.cast(Optional[str], self._stored.current_url) # type: ignore
|
||||
or self._get_url_from_relation_data()
|
||||
)
|
||||
return data
|
@ -1,12 +0,0 @@
|
||||
- project:
|
||||
templates:
|
||||
- charm-unit-jobs-py38
|
||||
- charm-unit-jobs-py310
|
||||
- charm-publish-jobs
|
||||
vars:
|
||||
needs_charm_build: true
|
||||
charm_build_name: cinder-ceph-k8s
|
||||
build_type: charmcraft
|
||||
publish_charm: true
|
||||
charmcraft_channel: 2.0/stable
|
||||
publish_channel: 2023.2/edge
|
@ -1,39 +0,0 @@
|
||||
# Copyright 2022 Canonical Ltd.
|
||||
# See LICENSE file for licensing details.
|
||||
|
||||
# Testing tools configuration
|
||||
[tool.coverage.run]
|
||||
branch = true
|
||||
|
||||
[tool.coverage.report]
|
||||
show_missing = true
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
minversion = "6.0"
|
||||
log_cli_level = "INFO"
|
||||
|
||||
# Formatting tools configuration
|
||||
[tool.black]
|
||||
line-length = 79
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
multi_line_output = 3
|
||||
force_grid_wrap = true
|
||||
|
||||
# Linting tools configuration
|
||||
[tool.flake8]
|
||||
max-line-length = 79
|
||||
max-doc-length = 99
|
||||
max-complexity = 10
|
||||
exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"]
|
||||
select = ["E", "W", "F", "C", "N", "R", "D", "H"]
|
||||
# Ignore W503, E501 because using black creates errors with this
|
||||
# Ignore D107 Missing docstring in __init__
|
||||
ignore = ["W503", "E501", "D107", "E402"]
|
||||
per-file-ignores = []
|
||||
docstring-convention = "google"
|
||||
# Check for properly formatted copyright header in each file
|
||||
copyright-check = "True"
|
||||
copyright-author = "Canonical Ltd."
|
||||
copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s"
|
@ -1,13 +0,0 @@
|
||||
#!/bin/bash
|
||||
charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}')
|
||||
echo "renaming ${charm}_*.charm to ${charm}.charm"
|
||||
echo -n "pwd: "
|
||||
pwd
|
||||
ls -al
|
||||
echo "Removing bad downloaded charm maybe?"
|
||||
if [[ -e "${charm}.charm" ]];
|
||||
then
|
||||
rm "${charm}.charm"
|
||||
fi
|
||||
echo "Renaming charm here."
|
||||
mv ${charm}_*.charm ${charm}.charm
|
@ -11,7 +11,6 @@ lightkube
|
||||
lightkube-models
|
||||
requests # Drop - not needed in storage backend interface.
|
||||
ops
|
||||
git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
|
||||
|
||||
git+https://opendev.org/openstack/charm-ops-interface-tls-certificates#egg=interface_tls_certificates
|
||||
|
||||
@ -23,3 +22,6 @@ git+https://github.com/juju/charm-helpers.git#egg=charmhelpers
|
||||
# TODO
|
||||
requests # Drop - not needed in storage backend interface.
|
||||
netifaces # Drop when charmhelpers dependency is removed.
|
||||
|
||||
# From ops_sunbeam
|
||||
tenacity
|
||||
|
@ -1,2 +0,0 @@
|
||||
[oslo_messaging_rabbit]
|
||||
rabbit_quorum_queue = True
|
@ -1,16 +0,0 @@
|
||||
# This file is managed centrally by release-tools and should not be modified
|
||||
# within individual charm repos. See the 'global' dir contents for available
|
||||
# choices of *requirements.txt files for OpenStack Charms:
|
||||
# https://github.com/openstack-charmers/release-tools
|
||||
#
|
||||
|
||||
coverage
|
||||
mock
|
||||
flake8
|
||||
stestr
|
||||
git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza
|
||||
git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack
|
||||
git+https://opendev.org/openstack/tempest.git#egg=tempest
|
||||
ops
|
||||
# Subunit 1.4.3+ requires extras
|
||||
extras
|
@ -18,16 +18,16 @@
|
||||
|
||||
import json
|
||||
|
||||
import charm
|
||||
import ops_sunbeam.test_utils as test_utils
|
||||
from mock import (
|
||||
MagicMock,
|
||||
patch,
|
||||
)
|
||||
from ops.testing import (
|
||||
Harness,
|
||||
)
|
||||
|
||||
import charm
|
||||
|
||||
|
||||
class _CinderCephOperatorCharm(charm.CinderCephOperatorCharm):
|
||||
"""Charm wrapper for test usage."""
|
||||
@ -82,6 +82,7 @@ class TestCinderCephOperatorCharm(test_utils.CharmTestCase):
|
||||
def setUp(self):
|
||||
"""Setup fixtures ready for testing."""
|
||||
super().setUp(charm, self.PATCHES)
|
||||
self.mock_event = MagicMock()
|
||||
self.harness = test_utils.get_harness(
|
||||
_CinderCephOperatorCharm, container_calls=self.container_calls
|
||||
)
|
||||
@ -118,7 +119,13 @@ class TestCinderCephOperatorCharm(test_utils.CharmTestCase):
|
||||
test_utils.add_complete_db_relation(self.harness)
|
||||
add_complete_storage_backend_relation(self.harness)
|
||||
test_utils.set_all_pebbles_ready(self.harness)
|
||||
self.assertTrue(self.harness.charm.relation_handlers_ready())
|
||||
self.assertSetEqual(
|
||||
self.harness.charm.get_mandatory_relations_not_ready(
|
||||
self.mock_event
|
||||
),
|
||||
set(),
|
||||
)
|
||||
# self.assertTrue(self.harness.charm.relation_handlers_ready())
|
||||
|
||||
def test_ceph_access(self):
|
||||
"""Test charm provides secret via ceph-access."""
|
||||
@ -132,7 +139,13 @@ class TestCinderCephOperatorCharm(test_utils.CharmTestCase):
|
||||
)
|
||||
add_complete_storage_backend_relation(self.harness)
|
||||
test_utils.set_all_pebbles_ready(self.harness)
|
||||
self.assertTrue(self.harness.charm.relation_handlers_ready())
|
||||
# self.assertTrue(self.harness.charm.relation_handlers_ready())
|
||||
self.assertSetEqual(
|
||||
self.harness.charm.get_mandatory_relations_not_ready(
|
||||
self.mock_event
|
||||
),
|
||||
set(),
|
||||
)
|
||||
rel_data = self.harness.get_relation_data(
|
||||
access_rel, self.harness.charm.unit.app.name
|
||||
)
|
||||
|
@ -1,160 +0,0 @@
|
||||
# Source charm: ./tox.ini
|
||||
# This file is managed centrally by release-tools and should not be modified
|
||||
# within individual charm repos. See the 'global' dir contents for available
|
||||
# choices of tox.ini for OpenStack Charms:
|
||||
# https://github.com/openstack-charmers/release-tools
|
||||
|
||||
[tox]
|
||||
skipsdist = True
|
||||
envlist = pep8,py3
|
||||
sitepackages = False
|
||||
skip_missing_interpreters = False
|
||||
minversion = 3.18.0
|
||||
|
||||
[vars]
|
||||
src_path = {toxinidir}/src/
|
||||
tst_path = {toxinidir}/tests/
|
||||
lib_path = {toxinidir}/lib/
|
||||
pyproject_toml = {toxinidir}/pyproject.toml
|
||||
all_path = {[vars]src_path} {[vars]tst_path}
|
||||
|
||||
[testenv]
|
||||
basepython = python3
|
||||
setenv =
|
||||
PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path}
|
||||
passenv =
|
||||
PYTHONPATH
|
||||
install_command =
|
||||
pip install {opts} {packages}
|
||||
commands = stestr run --slowest {posargs}
|
||||
allowlist_externals =
|
||||
git
|
||||
charmcraft
|
||||
{toxinidir}/fetch-libs.sh
|
||||
{toxinidir}/rename.sh
|
||||
deps =
|
||||
-r{toxinidir}/test-requirements.txt
|
||||
|
||||
[testenv:fmt]
|
||||
description = Apply coding style standards to code
|
||||
deps =
|
||||
black
|
||||
isort
|
||||
commands =
|
||||
isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox
|
||||
black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path}
|
||||
|
||||
[testenv:build]
|
||||
basepython = python3
|
||||
deps =
|
||||
commands =
|
||||
charmcraft -v pack
|
||||
{toxinidir}/rename.sh
|
||||
|
||||
[testenv:fetch]
|
||||
basepython = python3
|
||||
deps =
|
||||
commands =
|
||||
{toxinidir}/fetch-libs.sh
|
||||
|
||||
[testenv:py3]
|
||||
basepython = python3
|
||||
deps =
|
||||
{[testenv]deps}
|
||||
-r{toxinidir}/requirements.txt
|
||||
|
||||
[testenv:py38]
|
||||
basepython = python3.8
|
||||
deps = {[testenv:py3]deps}
|
||||
|
||||
[testenv:py39]
|
||||
basepython = python3.9
|
||||
deps = {[testenv:py3]deps}
|
||||
|
||||
[testenv:py310]
|
||||
basepython = python3.10
|
||||
deps = {[testenv:py3]deps}
|
||||
|
||||
[testenv:py311]
|
||||
basepython = python3.11
|
||||
deps = {[testenv:py3]deps}
|
||||
|
||||
[testenv:cover]
|
||||
basepython = python3
|
||||
deps = {[testenv:py3]deps}
|
||||
setenv =
|
||||
{[testenv]setenv}
|
||||
PYTHON=coverage run
|
||||
commands =
|
||||
coverage erase
|
||||
stestr run --slowest {posargs}
|
||||
coverage combine
|
||||
coverage html -d cover
|
||||
coverage xml -o cover/coverage.xml
|
||||
coverage report
|
||||
|
||||
[testenv:pep8]
|
||||
description = Alias for lint
|
||||
deps = {[testenv:lint]deps}
|
||||
commands = {[testenv:lint]commands}
|
||||
|
||||
[testenv:lint]
|
||||
description = Check code against coding style standards
|
||||
deps =
|
||||
black
|
||||
flake8<6
|
||||
flake8-docstrings
|
||||
flake8-copyright
|
||||
flake8-builtins
|
||||
pyproject-flake8
|
||||
pep8-naming
|
||||
isort
|
||||
codespell
|
||||
commands =
|
||||
codespell {[vars]all_path}
|
||||
# pflake8 wrapper supports config from pyproject.toml
|
||||
pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path}
|
||||
isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path}
|
||||
black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path}
|
||||
|
||||
[testenv:func-noop]
|
||||
basepython = python3
|
||||
commands =
|
||||
functest-run-suite --help
|
||||
|
||||
[testenv:func]
|
||||
basepython = python3
|
||||
commands =
|
||||
functest-run-suite --keep-model
|
||||
|
||||
[testenv:func-smoke]
|
||||
basepython = python3
|
||||
setenv =
|
||||
TEST_MODEL_SETTINGS = automatically-retry-hooks=true
|
||||
TEST_MAX_RESOLVE_COUNT = 5
|
||||
commands =
|
||||
functest-run-suite --keep-model --smoke
|
||||
|
||||
[testenv:func-dev]
|
||||
basepython = python3
|
||||
commands =
|
||||
functest-run-suite --keep-model --dev
|
||||
|
||||
[testenv:func-target]
|
||||
basepython = python3
|
||||
commands =
|
||||
functest-run-suite --keep-model --bundle {posargs}
|
||||
|
||||
[coverage:run]
|
||||
branch = True
|
||||
concurrency = multiprocessing
|
||||
parallel = True
|
||||
source =
|
||||
.
|
||||
omit =
|
||||
.tox/*
|
||||
tests/*
|
||||
src/templates/*
|
||||
|
||||
[flake8]
|
||||
ignore=E226,W504
|
@ -1,9 +0,0 @@
|
||||
[flake8]
|
||||
max-line-length = 99
|
||||
select: E,W,F,C,N
|
||||
exclude:
|
||||
venv
|
||||
.git
|
||||
build
|
||||
dist
|
||||
*.egg_info
|
8
charms/cinder-k8s/.gitignore
vendored
8
charms/cinder-k8s/.gitignore
vendored
@ -1,8 +0,0 @@
|
||||
venv/
|
||||
build/
|
||||
.stestr/
|
||||
*.charm
|
||||
.tox
|
||||
.coverage
|
||||
__pycache__/
|
||||
*.py[cod]
|
@ -1,5 +0,0 @@
|
||||
[gerrit]
|
||||
host=review.opendev.org
|
||||
port=29418
|
||||
project=openstack/charm-cinder-k8s.git
|
||||
defaultbranch=main
|
@ -1,3 +0,0 @@
|
||||
/venv
|
||||
*.py[cod]
|
||||
*.charm
|
@ -1,3 +0,0 @@
|
||||
[DEFAULT]
|
||||
test_path=./tests/unit
|
||||
top_dir=./tests
|
@ -1,11 +0,0 @@
|
||||
- project:
|
||||
templates:
|
||||
- openstack-python3-charm-jobs
|
||||
- openstack-cover-jobs
|
||||
- microk8s-func-test
|
||||
vars:
|
||||
charm_build_name: cinder-k8s
|
||||
juju_channel: 3.1/stable
|
||||
juju_classic_mode: false
|
||||
microk8s_channel: 1.28-strict/stable
|
||||
microk8s_classic_mode: false
|
@ -28,4 +28,3 @@ parts:
|
||||
- jsonschema
|
||||
- pydantic<2.0
|
||||
- jinja2
|
||||
- git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
|
||||
|
@ -1,7 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "INFO: Fetching libs from charmhub."
|
||||
charmcraft fetch-lib charms.data_platform_libs.v0.database_requires
|
||||
charmcraft fetch-lib charms.keystone_k8s.v1.identity_service
|
||||
charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq
|
||||
charmcraft fetch-lib charms.traefik_k8s.v2.ingress
|
@ -1,189 +0,0 @@
|
||||
"""TODO: Add a proper docstring here.
|
||||
|
||||
This is a placeholder docstring for this charm library. Docstrings are
|
||||
presented on Charmhub and updated whenever you push a new version of the
|
||||
library.
|
||||
|
||||
Complete documentation about creating and documenting libraries can be found
|
||||
in the SDK docs at https://juju.is/docs/sdk/libraries.
|
||||
|
||||
See `charmcraft publish-lib` and `charmcraft fetch-lib` for details of how to
|
||||
share and consume charm libraries. They serve to enhance collaboration
|
||||
between charmers. Use a charmer's libraries for classes that handle
|
||||
integration with their charm.
|
||||
|
||||
Bear in mind that new revisions of the different major API versions (v0, v1,
|
||||
v2 etc) are maintained independently. You can continue to update v0 and v1
|
||||
after you have pushed v3.
|
||||
|
||||
Markdown is supported, following the CommonMark specification.
|
||||
"""
|
||||
|
||||
# The unique Charmhub library identifier, never change it
|
||||
LIBID = "68536ea2f06d40078ccbedd7095e141c"
|
||||
|
||||
# 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
|
||||
|
||||
import json
|
||||
import logging
|
||||
import requests
|
||||
|
||||
from ops.framework import (
|
||||
StoredState,
|
||||
EventBase,
|
||||
ObjectEvents,
|
||||
EventSource,
|
||||
Object,
|
||||
)
|
||||
|
||||
from ops.model import Relation
|
||||
|
||||
from typing import List
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# TODO: add your code here! Happy coding!
|
||||
class StorageBackendConnectedEvent(EventBase):
|
||||
"""StorageBackend connected Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class StorageBackendReadyEvent(EventBase):
|
||||
"""StorageBackend ready for use Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class StorageBackendGoneAwayEvent(EventBase):
|
||||
"""StorageBackend relation has gone-away Event"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class StorageBackendServerEvents(ObjectEvents):
|
||||
"""Events class for `on`"""
|
||||
|
||||
connected = EventSource(StorageBackendConnectedEvent)
|
||||
ready = EventSource(StorageBackendReadyEvent)
|
||||
goneaway = EventSource(StorageBackendGoneAwayEvent)
|
||||
|
||||
|
||||
class StorageBackendRequires(Object):
|
||||
"""
|
||||
StorageBackendRequires class
|
||||
"""
|
||||
|
||||
on = StorageBackendServerEvents()
|
||||
_stored = StoredState()
|
||||
|
||||
def __init__(self, charm, relation_name: str):
|
||||
super().__init__(charm, relation_name)
|
||||
self.charm = charm
|
||||
self.relation_name = relation_name
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_joined,
|
||||
self._on_storage_backend_relation_joined,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_changed,
|
||||
self._on_storage_backend_relation_changed,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_departed,
|
||||
self._on_storage_backend_relation_changed,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_broken,
|
||||
self._on_storage_backend_relation_broken,
|
||||
)
|
||||
|
||||
def _on_storage_backend_relation_joined(self, event):
|
||||
"""StorageBackend relation joined."""
|
||||
logging.debug("StorageBackendRequires on_joined")
|
||||
self.on.connected.emit()
|
||||
|
||||
def _on_storage_backend_relation_changed(self, event):
|
||||
"""StorageBackend relation changed."""
|
||||
logging.debug("StorageBackendRequires on_changed")
|
||||
self.on.ready.emit()
|
||||
|
||||
def _on_storage_backend_relation_broken(self, event):
|
||||
"""StorageBackend relation broken."""
|
||||
logging.debug("StorageBackendRequires on_broken")
|
||||
self.on.goneaway.emit()
|
||||
|
||||
def set_ready(self) -> None:
|
||||
"""Request access to the StorageBackend server."""
|
||||
if self.model.unit.is_leader():
|
||||
logging.debug(
|
||||
"Signalling storage backends that core services are ready"
|
||||
)
|
||||
for relation in self.framework.model.relations[self.relation_name]:
|
||||
relation.data[self.charm.app]["ready"] = 'true'
|
||||
|
||||
|
||||
class APIReadyEvent(EventBase):
|
||||
"""StorageBackendClients Ready Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class StorageBackendClientEvents(ObjectEvents):
|
||||
"""Events class for `on`"""
|
||||
|
||||
api_ready = EventSource(APIReadyEvent)
|
||||
|
||||
|
||||
class StorageBackendProvides(Object):
|
||||
"""
|
||||
StorageBackendProvides class
|
||||
"""
|
||||
|
||||
on = StorageBackendClientEvents()
|
||||
_stored = StoredState()
|
||||
|
||||
def __init__(self, charm, relation_name):
|
||||
super().__init__(charm, relation_name)
|
||||
self.charm = charm
|
||||
self.relation_name = relation_name
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_joined,
|
||||
self._on_storage_backend_relation_joined,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_changed,
|
||||
self._on_storage_backend_relation_changed,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_broken,
|
||||
self._on_storage_backend_relation_broken,
|
||||
)
|
||||
|
||||
def _on_storage_backend_relation_joined(self, event):
|
||||
"""Handle StorageBackend joined."""
|
||||
logging.debug("StorageBackendProvides on_joined")
|
||||
|
||||
def remote_ready(self):
|
||||
relation = self.framework.model.get_relation(self.relation_name)
|
||||
if relation:
|
||||
ready = relation.data[relation.app].get("ready")
|
||||
return ready and json.loads(ready)
|
||||
return False
|
||||
|
||||
def _on_storage_backend_relation_changed(self, event):
|
||||
"""Handle StorageBackend changed."""
|
||||
logging.debug("StorageBackendProvides on_changed")
|
||||
if self.remote_ready():
|
||||
self.on.api_ready.emit()
|
||||
|
||||
def _on_storage_backend_relation_broken(self, event):
|
||||
"""Handle StorageBackend broken."""
|
||||
logging.debug("RabbitMQStorageBackendProvides on_departed")
|
||||
# TODO clear data on the relation
|
@ -1,496 +0,0 @@
|
||||
# Copyright 2022 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.
|
||||
|
||||
"""Relation 'requires' side abstraction for database relation.
|
||||
|
||||
This library is a uniform interface to a selection of common database
|
||||
metadata, with added custom events that add convenience to database management,
|
||||
and methods to consume the application related data.
|
||||
|
||||
Following an example of using the DatabaseCreatedEvent, in the context of the
|
||||
application charm code:
|
||||
|
||||
```python
|
||||
|
||||
from charms.data_platform_libs.v0.database_requires import DatabaseRequires
|
||||
|
||||
class ApplicationCharm(CharmBase):
|
||||
# Application charm that connects to database charms.
|
||||
|
||||
def __init__(self, *args):
|
||||
super().__init__(*args)
|
||||
|
||||
# Charm events defined in the database requires charm library.
|
||||
self.database = DatabaseRequires(self, relation_name="database", database_name="database")
|
||||
self.framework.observe(self.database.on.database_created, self._on_database_created)
|
||||
|
||||
def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
|
||||
# Handle the created database
|
||||
|
||||
# Create configuration file for app
|
||||
config_file = self._render_app_config_file(
|
||||
event.username,
|
||||
event.password,
|
||||
event.endpoints,
|
||||
)
|
||||
|
||||
# Start application with rendered configuration
|
||||
self._start_application(config_file)
|
||||
|
||||
# Set active status
|
||||
self.unit.status = ActiveStatus("received database credentials")
|
||||
```
|
||||
|
||||
As shown above, the library provides some custom events to handle specific situations,
|
||||
which are listed below:
|
||||
|
||||
— database_created: event emitted when the requested database is created.
|
||||
— endpoints_changed: event emitted when the read/write endpoints of the database have changed.
|
||||
— read_only_endpoints_changed: event emitted when the read-only endpoints of the database
|
||||
have changed. Event is not triggered if read/write endpoints changed too.
|
||||
|
||||
If it is needed to connect multiple database clusters to the same relation endpoint
|
||||
the application charm can implement the same code as if it would connect to only
|
||||
one database cluster (like the above code example).
|
||||
|
||||
To differentiate multiple clusters connected to the same relation endpoint
|
||||
the application charm can use the name of the remote application:
|
||||
|
||||
```python
|
||||
|
||||
def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
|
||||
# Get the remote app name of the cluster that triggered this event
|
||||
cluster = event.relation.app.name
|
||||
```
|
||||
|
||||
It is also possible to provide an alias for each different database cluster/relation.
|
||||
|
||||
So, it is possible to differentiate the clusters in two ways.
|
||||
The first is to use the remote application name, i.e., `event.relation.app.name`, as above.
|
||||
|
||||
The second way is to use different event handlers to handle each cluster events.
|
||||
The implementation would be something like the following code:
|
||||
|
||||
```python
|
||||
|
||||
from charms.data_platform_libs.v0.database_requires import DatabaseRequires
|
||||
|
||||
class ApplicationCharm(CharmBase):
|
||||
# Application charm that connects to database charms.
|
||||
|
||||
def __init__(self, *args):
|
||||
super().__init__(*args)
|
||||
|
||||
# Define the cluster aliases and one handler for each cluster database created event.
|
||||
self.database = DatabaseRequires(
|
||||
self,
|
||||
relation_name="database",
|
||||
database_name="database",
|
||||
relations_aliases = ["cluster1", "cluster2"],
|
||||
)
|
||||
self.framework.observe(
|
||||
self.database.on.cluster1_database_created, self._on_cluster1_database_created
|
||||
)
|
||||
self.framework.observe(
|
||||
self.database.on.cluster2_database_created, self._on_cluster2_database_created
|
||||
)
|
||||
|
||||
def _on_cluster1_database_created(self, event: DatabaseCreatedEvent) -> None:
|
||||
# Handle the created database on the cluster named cluster1
|
||||
|
||||
# Create configuration file for app
|
||||
config_file = self._render_app_config_file(
|
||||
event.username,
|
||||
event.password,
|
||||
event.endpoints,
|
||||
)
|
||||
...
|
||||
|
||||
def _on_cluster2_database_created(self, event: DatabaseCreatedEvent) -> None:
|
||||
# Handle the created database on the cluster named cluster2
|
||||
|
||||
# Create configuration file for app
|
||||
config_file = self._render_app_config_file(
|
||||
event.username,
|
||||
event.password,
|
||||
event.endpoints,
|
||||
)
|
||||
...
|
||||
|
||||
```
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from collections import namedtuple
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
from ops.charm import (
|
||||
CharmEvents,
|
||||
RelationChangedEvent,
|
||||
RelationEvent,
|
||||
RelationJoinedEvent,
|
||||
)
|
||||
from ops.framework import EventSource, Object
|
||||
from ops.model import Relation
|
||||
|
||||
# The unique Charmhub library identifier, never change it
|
||||
LIBID = "0241e088ffa9440fb4e3126349b2fb62"
|
||||
|
||||
# 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 = 4
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DatabaseEvent(RelationEvent):
|
||||
"""Base class for database events."""
|
||||
|
||||
@property
|
||||
def endpoints(self) -> Optional[str]:
|
||||
"""Returns a comma separated list of read/write endpoints."""
|
||||
return self.relation.data[self.relation.app].get("endpoints")
|
||||
|
||||
@property
|
||||
def password(self) -> Optional[str]:
|
||||
"""Returns the password for the created user."""
|
||||
return self.relation.data[self.relation.app].get("password")
|
||||
|
||||
@property
|
||||
def read_only_endpoints(self) -> Optional[str]:
|
||||
"""Returns a comma separated list of read only endpoints."""
|
||||
return self.relation.data[self.relation.app].get("read-only-endpoints")
|
||||
|
||||
@property
|
||||
def replset(self) -> Optional[str]:
|
||||
"""Returns the replicaset name.
|
||||
|
||||
MongoDB only.
|
||||
"""
|
||||
return self.relation.data[self.relation.app].get("replset")
|
||||
|
||||
@property
|
||||
def tls(self) -> Optional[str]:
|
||||
"""Returns whether TLS is configured."""
|
||||
return self.relation.data[self.relation.app].get("tls")
|
||||
|
||||
@property
|
||||
def tls_ca(self) -> Optional[str]:
|
||||
"""Returns TLS CA."""
|
||||
return self.relation.data[self.relation.app].get("tls-ca")
|
||||
|
||||
@property
|
||||
def uris(self) -> Optional[str]:
|
||||
"""Returns the connection URIs.
|
||||
|
||||
MongoDB, Redis, OpenSearch and Kafka only.
|
||||
"""
|
||||
return self.relation.data[self.relation.app].get("uris")
|
||||
|
||||
@property
|
||||
def username(self) -> Optional[str]:
|
||||
"""Returns the created username."""
|
||||
return self.relation.data[self.relation.app].get("username")
|
||||
|
||||
@property
|
||||
def version(self) -> Optional[str]:
|
||||
"""Returns the version of the database.
|
||||
|
||||
Version as informed by the database daemon.
|
||||
"""
|
||||
return self.relation.data[self.relation.app].get("version")
|
||||
|
||||
|
||||
class DatabaseCreatedEvent(DatabaseEvent):
|
||||
"""Event emitted when a new database is created for use on this relation."""
|
||||
|
||||
|
||||
class DatabaseEndpointsChangedEvent(DatabaseEvent):
|
||||
"""Event emitted when the read/write endpoints are changed."""
|
||||
|
||||
|
||||
class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent):
|
||||
"""Event emitted when the read only endpoints are changed."""
|
||||
|
||||
|
||||
class DatabaseEvents(CharmEvents):
|
||||
"""Database events.
|
||||
|
||||
This class defines the events that the database can emit.
|
||||
"""
|
||||
|
||||
database_created = EventSource(DatabaseCreatedEvent)
|
||||
endpoints_changed = EventSource(DatabaseEndpointsChangedEvent)
|
||||
read_only_endpoints_changed = EventSource(DatabaseReadOnlyEndpointsChangedEvent)
|
||||
|
||||
|
||||
Diff = namedtuple("Diff", "added changed deleted")
|
||||
Diff.__doc__ = """
|
||||
A tuple for storing the diff between two data mappings.
|
||||
|
||||
— added — keys that were added.
|
||||
— changed — keys that still exist but have new values.
|
||||
— deleted — keys that were deleted.
|
||||
"""
|
||||
|
||||
|
||||
class DatabaseRequires(Object):
|
||||
"""Requires-side of the database relation."""
|
||||
|
||||
on = DatabaseEvents()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
charm,
|
||||
relation_name: str,
|
||||
database_name: str,
|
||||
extra_user_roles: str = None,
|
||||
relations_aliases: List[str] = None,
|
||||
):
|
||||
"""Manager of database client relations."""
|
||||
super().__init__(charm, relation_name)
|
||||
self.charm = charm
|
||||
self.database = database_name
|
||||
self.extra_user_roles = extra_user_roles
|
||||
self.local_app = self.charm.model.app
|
||||
self.local_unit = self.charm.unit
|
||||
self.relation_name = relation_name
|
||||
self.relations_aliases = relations_aliases
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_joined, self._on_relation_joined_event
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_changed, self._on_relation_changed_event
|
||||
)
|
||||
|
||||
# Define custom event names for each alias.
|
||||
if relations_aliases:
|
||||
# Ensure the number of aliases does not exceed the maximum
|
||||
# of connections allowed in the specific relation.
|
||||
relation_connection_limit = self.charm.meta.requires[relation_name].limit
|
||||
if len(relations_aliases) != relation_connection_limit:
|
||||
raise ValueError(
|
||||
f"The number of aliases must match the maximum number of connections allowed in the relation. "
|
||||
f"Expected {relation_connection_limit}, got {len(relations_aliases)}"
|
||||
)
|
||||
|
||||
for relation_alias in relations_aliases:
|
||||
self.on.define_event(f"{relation_alias}_database_created", DatabaseCreatedEvent)
|
||||
self.on.define_event(
|
||||
f"{relation_alias}_endpoints_changed", DatabaseEndpointsChangedEvent
|
||||
)
|
||||
self.on.define_event(
|
||||
f"{relation_alias}_read_only_endpoints_changed",
|
||||
DatabaseReadOnlyEndpointsChangedEvent,
|
||||
)
|
||||
|
||||
def _assign_relation_alias(self, relation_id: int) -> None:
|
||||
"""Assigns an alias to a relation.
|
||||
|
||||
This function writes in the unit data bag.
|
||||
|
||||
Args:
|
||||
relation_id: the identifier for a particular relation.
|
||||
"""
|
||||
# If no aliases were provided, return immediately.
|
||||
if not self.relations_aliases:
|
||||
return
|
||||
|
||||
# Return if an alias was already assigned to this relation
|
||||
# (like when there are more than one unit joining the relation).
|
||||
if (
|
||||
self.charm.model.get_relation(self.relation_name, relation_id)
|
||||
.data[self.local_unit]
|
||||
.get("alias")
|
||||
):
|
||||
return
|
||||
|
||||
# Retrieve the available aliases (the ones that weren't assigned to any relation).
|
||||
available_aliases = self.relations_aliases[:]
|
||||
for relation in self.charm.model.relations[self.relation_name]:
|
||||
alias = relation.data[self.local_unit].get("alias")
|
||||
if alias:
|
||||
logger.debug("Alias %s was already assigned to relation %d", alias, relation.id)
|
||||
available_aliases.remove(alias)
|
||||
|
||||
# Set the alias in the unit relation databag of the specific relation.
|
||||
relation = self.charm.model.get_relation(self.relation_name, relation_id)
|
||||
relation.data[self.local_unit].update({"alias": available_aliases[0]})
|
||||
|
||||
def _diff(self, event: RelationChangedEvent) -> Diff:
|
||||
"""Retrieves the diff of the data in the relation changed databag.
|
||||
|
||||
Args:
|
||||
event: relation changed event.
|
||||
|
||||
Returns:
|
||||
a Diff instance containing the added, deleted and changed
|
||||
keys from the event relation databag.
|
||||
"""
|
||||
# Retrieve the old data from the data key in the local unit relation databag.
|
||||
old_data = json.loads(event.relation.data[self.local_unit].get("data", "{}"))
|
||||
# Retrieve the new data from the event relation databag.
|
||||
new_data = {
|
||||
key: value for key, value in event.relation.data[event.app].items() if key != "data"
|
||||
}
|
||||
|
||||
# These are the keys that were added to the databag and triggered this event.
|
||||
added = new_data.keys() - old_data.keys()
|
||||
# These are the keys that were removed from the databag and triggered this event.
|
||||
deleted = old_data.keys() - new_data.keys()
|
||||
# These are the keys that already existed in the databag,
|
||||
# but had their values changed.
|
||||
changed = {
|
||||
key for key in old_data.keys() & new_data.keys() if old_data[key] != new_data[key]
|
||||
}
|
||||
|
||||
# TODO: evaluate the possibility of losing the diff if some error
|
||||
# happens in the charm before the diff is completely checked (DPE-412).
|
||||
# Convert the new_data to a serializable format and save it for a next diff check.
|
||||
event.relation.data[self.local_unit].update({"data": json.dumps(new_data)})
|
||||
|
||||
# Return the diff with all possible changes.
|
||||
return Diff(added, changed, deleted)
|
||||
|
||||
def _emit_aliased_event(self, event: RelationChangedEvent, event_name: str) -> None:
|
||||
"""Emit an aliased event to a particular relation if it has an alias.
|
||||
|
||||
Args:
|
||||
event: the relation changed event that was received.
|
||||
event_name: the name of the event to emit.
|
||||
"""
|
||||
alias = self._get_relation_alias(event.relation.id)
|
||||
if alias:
|
||||
getattr(self.on, f"{alias}_{event_name}").emit(
|
||||
event.relation, app=event.app, unit=event.unit
|
||||
)
|
||||
|
||||
def _get_relation_alias(self, relation_id: int) -> Optional[str]:
|
||||
"""Returns the relation alias.
|
||||
|
||||
Args:
|
||||
relation_id: the identifier for a particular relation.
|
||||
|
||||
Returns:
|
||||
the relation alias or None if the relation was not found.
|
||||
"""
|
||||
for relation in self.charm.model.relations[self.relation_name]:
|
||||
if relation.id == relation_id:
|
||||
return relation.data[self.local_unit].get("alias")
|
||||
return None
|
||||
|
||||
def fetch_relation_data(self) -> dict:
|
||||
"""Retrieves data from relation.
|
||||
|
||||
This function can be used to retrieve data from a relation
|
||||
in the charm code when outside an event callback.
|
||||
|
||||
Returns:
|
||||
a dict of the values stored in the relation data bag
|
||||
for all relation instances (indexed by the relation ID).
|
||||
"""
|
||||
data = {}
|
||||
for relation in self.relations:
|
||||
data[relation.id] = {
|
||||
key: value for key, value in relation.data[relation.app].items() if key != "data"
|
||||
}
|
||||
return data
|
||||
|
||||
def _update_relation_data(self, relation_id: int, data: dict) -> None:
|
||||
"""Updates a set of key-value pairs in the relation.
|
||||
|
||||
This function writes in the application data bag, therefore,
|
||||
only the leader unit can call it.
|
||||
|
||||
Args:
|
||||
relation_id: the identifier for a particular relation.
|
||||
data: dict containing the key-value pairs
|
||||
that should be updated in the relation.
|
||||
"""
|
||||
if self.local_unit.is_leader():
|
||||
relation = self.charm.model.get_relation(self.relation_name, relation_id)
|
||||
relation.data[self.local_app].update(data)
|
||||
|
||||
def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None:
|
||||
"""Event emitted when the application joins the database relation."""
|
||||
# If relations aliases were provided, assign one to the relation.
|
||||
self._assign_relation_alias(event.relation.id)
|
||||
|
||||
# Sets both database and extra user roles in the relation
|
||||
# if the roles are provided. Otherwise, sets only the database.
|
||||
if self.extra_user_roles:
|
||||
self._update_relation_data(
|
||||
event.relation.id,
|
||||
{
|
||||
"database": self.database,
|
||||
"extra-user-roles": self.extra_user_roles,
|
||||
},
|
||||
)
|
||||
else:
|
||||
self._update_relation_data(event.relation.id, {"database": self.database})
|
||||
|
||||
def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
|
||||
"""Event emitted when the database relation has changed."""
|
||||
# Check which data has changed to emit customs events.
|
||||
diff = self._diff(event)
|
||||
|
||||
# Check if the database is created
|
||||
# (the database charm shared the credentials).
|
||||
if "username" in diff.added and "password" in diff.added:
|
||||
# Emit the default event (the one without an alias).
|
||||
logger.info("database created at %s", datetime.now())
|
||||
self.on.database_created.emit(event.relation, app=event.app, unit=event.unit)
|
||||
|
||||
# Emit the aliased event (if any).
|
||||
self._emit_aliased_event(event, "database_created")
|
||||
|
||||
# To avoid unnecessary application restarts do not trigger
|
||||
# “endpoints_changed“ event if “database_created“ is triggered.
|
||||
return
|
||||
|
||||
# Emit an endpoints changed event if the database
|
||||
# added or changed this info in the relation databag.
|
||||
if "endpoints" in diff.added or "endpoints" in diff.changed:
|
||||
# Emit the default event (the one without an alias).
|
||||
logger.info("endpoints changed on %s", datetime.now())
|
||||
self.on.endpoints_changed.emit(event.relation, app=event.app, unit=event.unit)
|
||||
|
||||
# Emit the aliased event (if any).
|
||||
self._emit_aliased_event(event, "endpoints_changed")
|
||||
|
||||
# To avoid unnecessary application restarts do not trigger
|
||||
# “read_only_endpoints_changed“ event if “endpoints_changed“ is triggered.
|
||||
return
|
||||
|
||||
# Emit a read only endpoints changed event if the database
|
||||
# added or changed this info in the relation databag.
|
||||
if "read-only-endpoints" in diff.added or "read-only-endpoints" in diff.changed:
|
||||
# Emit the default event (the one without an alias).
|
||||
logger.info("read-only-endpoints changed on %s", datetime.now())
|
||||
self.on.read_only_endpoints_changed.emit(
|
||||
event.relation, app=event.app, unit=event.unit
|
||||
)
|
||||
|
||||
# Emit the aliased event (if any).
|
||||
self._emit_aliased_event(event, "read_only_endpoints_changed")
|
||||
|
||||
@property
|
||||
def relations(self) -> List[Relation]:
|
||||
"""The list of Relation instances associated with this relation_name."""
|
||||
return list(self.charm.model.relations[self.relation_name])
|
@ -1,525 +0,0 @@
|
||||
"""IdentityServiceProvides and Requires module.
|
||||
|
||||
|
||||
This library contains the Requires and Provides classes for handling
|
||||
the identity_service interface.
|
||||
|
||||
Import `IdentityServiceRequires` in your charm, with the charm object and the
|
||||
relation name:
|
||||
- self
|
||||
- "identity_service"
|
||||
|
||||
Also provide additional parameters to the charm object:
|
||||
- service
|
||||
- internal_url
|
||||
- public_url
|
||||
- admin_url
|
||||
- region
|
||||
- username
|
||||
- vhost
|
||||
|
||||
Two events are also available to respond to:
|
||||
- connected
|
||||
- ready
|
||||
- goneaway
|
||||
|
||||
A basic example showing the usage of this relation follows:
|
||||
|
||||
```
|
||||
from charms.keystone_k8s.v1.identity_service import IdentityServiceRequires
|
||||
|
||||
class IdentityServiceClientCharm(CharmBase):
|
||||
def __init__(self, *args):
|
||||
super().__init__(*args)
|
||||
# IdentityService Requires
|
||||
self.identity_service = IdentityServiceRequires(
|
||||
self, "identity_service",
|
||||
service = "my-service"
|
||||
internal_url = "http://internal-url"
|
||||
public_url = "http://public-url"
|
||||
admin_url = "http://admin-url"
|
||||
region = "region"
|
||||
)
|
||||
self.framework.observe(
|
||||
self.identity_service.on.connected, self._on_identity_service_connected)
|
||||
self.framework.observe(
|
||||
self.identity_service.on.ready, self._on_identity_service_ready)
|
||||
self.framework.observe(
|
||||
self.identity_service.on.goneaway, self._on_identity_service_goneaway)
|
||||
|
||||
def _on_identity_service_connected(self, event):
|
||||
'''React to the IdentityService connected event.
|
||||
|
||||
This event happens when n IdentityService relation is added to the
|
||||
model before credentials etc have been provided.
|
||||
'''
|
||||
# Do something before the relation is complete
|
||||
pass
|
||||
|
||||
def _on_identity_service_ready(self, event):
|
||||
'''React to the IdentityService ready event.
|
||||
|
||||
The IdentityService interface will use the provided config for the
|
||||
request to the identity server.
|
||||
'''
|
||||
# IdentityService Relation is ready. Do something with the completed relation.
|
||||
pass
|
||||
|
||||
def _on_identity_service_goneaway(self, event):
|
||||
'''React to the IdentityService goneaway event.
|
||||
|
||||
This event happens when an IdentityService relation is removed.
|
||||
'''
|
||||
# IdentityService Relation has goneaway. shutdown services or suchlike
|
||||
pass
|
||||
```
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
from ops.framework import (
|
||||
StoredState,
|
||||
EventBase,
|
||||
ObjectEvents,
|
||||
EventSource,
|
||||
Object,
|
||||
)
|
||||
from ops.model import (
|
||||
Relation,
|
||||
SecretNotFoundError,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# The unique Charmhub library identifier, never change it
|
||||
LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0"
|
||||
|
||||
# Increment this major API version when introducing breaking changes
|
||||
LIBAPI = 1
|
||||
|
||||
# Increment this PATCH version before using `charmcraft publish-lib` or reset
|
||||
# to 0 if you are raising the major API version
|
||||
LIBPATCH = 1
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IdentityServiceConnectedEvent(EventBase):
|
||||
"""IdentityService connected Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class IdentityServiceReadyEvent(EventBase):
|
||||
"""IdentityService ready for use Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class IdentityServiceGoneAwayEvent(EventBase):
|
||||
"""IdentityService relation has gone-away Event"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class IdentityServiceServerEvents(ObjectEvents):
|
||||
"""Events class for `on`"""
|
||||
|
||||
connected = EventSource(IdentityServiceConnectedEvent)
|
||||
ready = EventSource(IdentityServiceReadyEvent)
|
||||
goneaway = EventSource(IdentityServiceGoneAwayEvent)
|
||||
|
||||
|
||||
class IdentityServiceRequires(Object):
|
||||
"""
|
||||
IdentityServiceRequires class
|
||||
"""
|
||||
|
||||
on = IdentityServiceServerEvents()
|
||||
_stored = StoredState()
|
||||
|
||||
def __init__(self, charm, relation_name: str, service_endpoints: dict,
|
||||
region: str):
|
||||
super().__init__(charm, relation_name)
|
||||
self.charm = charm
|
||||
self.relation_name = relation_name
|
||||
self.service_endpoints = service_endpoints
|
||||
self.region = region
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_joined,
|
||||
self._on_identity_service_relation_joined,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_changed,
|
||||
self._on_identity_service_relation_changed,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_departed,
|
||||
self._on_identity_service_relation_changed,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_broken,
|
||||
self._on_identity_service_relation_broken,
|
||||
)
|
||||
|
||||
def _on_identity_service_relation_joined(self, event):
|
||||
"""IdentityService relation joined."""
|
||||
logging.debug("IdentityService on_joined")
|
||||
self.on.connected.emit()
|
||||
self.register_services(
|
||||
self.service_endpoints,
|
||||
self.region)
|
||||
|
||||
def _on_identity_service_relation_changed(self, event):
|
||||
"""IdentityService relation changed."""
|
||||
logging.debug("IdentityService on_changed")
|
||||
try:
|
||||
self.service_password
|
||||
self.on.ready.emit()
|
||||
except (AttributeError, KeyError):
|
||||
pass
|
||||
|
||||
def _on_identity_service_relation_broken(self, event):
|
||||
"""IdentityService relation broken."""
|
||||
logging.debug("IdentityService on_broken")
|
||||
self.on.goneaway.emit()
|
||||
|
||||
@property
|
||||
def _identity_service_rel(self) -> Relation:
|
||||
"""The IdentityService relation."""
|
||||
return self.framework.model.get_relation(self.relation_name)
|
||||
|
||||
def get_remote_app_data(self, key: str) -> str:
|
||||
"""Return the value for the given key from remote app data."""
|
||||
data = self._identity_service_rel.data[self._identity_service_rel.app]
|
||||
return data.get(key)
|
||||
|
||||
@property
|
||||
def api_version(self) -> str:
|
||||
"""Return the api_version."""
|
||||
return self.get_remote_app_data('api-version')
|
||||
|
||||
@property
|
||||
def auth_host(self) -> str:
|
||||
"""Return the auth_host."""
|
||||
return self.get_remote_app_data('auth-host')
|
||||
|
||||
@property
|
||||
def auth_port(self) -> str:
|
||||
"""Return the auth_port."""
|
||||
return self.get_remote_app_data('auth-port')
|
||||
|
||||
@property
|
||||
def auth_protocol(self) -> str:
|
||||
"""Return the auth_protocol."""
|
||||
return self.get_remote_app_data('auth-protocol')
|
||||
|
||||
@property
|
||||
def internal_host(self) -> str:
|
||||
"""Return the internal_host."""
|
||||
return self.get_remote_app_data('internal-host')
|
||||
|
||||
@property
|
||||
def internal_port(self) -> str:
|
||||
"""Return the internal_port."""
|
||||
return self.get_remote_app_data('internal-port')
|
||||
|
||||
@property
|
||||
def internal_protocol(self) -> str:
|
||||
"""Return the internal_protocol."""
|
||||
return self.get_remote_app_data('internal-protocol')
|
||||
|
||||
@property
|
||||
def admin_domain_name(self) -> str:
|
||||
"""Return the admin_domain_name."""
|
||||
return self.get_remote_app_data('admin-domain-name')
|
||||
|
||||
@property
|
||||
def admin_domain_id(self) -> str:
|
||||
"""Return the admin_domain_id."""
|
||||
return self.get_remote_app_data('admin-domain-id')
|
||||
|
||||
@property
|
||||
def admin_project_name(self) -> str:
|
||||
"""Return the admin_project_name."""
|
||||
return self.get_remote_app_data('admin-project-name')
|
||||
|
||||
@property
|
||||
def admin_project_id(self) -> str:
|
||||
"""Return the admin_project_id."""
|
||||
return self.get_remote_app_data('admin-project-id')
|
||||
|
||||
@property
|
||||
def admin_user_name(self) -> str:
|
||||
"""Return the admin_user_name."""
|
||||
return self.get_remote_app_data('admin-user-name')
|
||||
|
||||
@property
|
||||
def admin_user_id(self) -> str:
|
||||
"""Return the admin_user_id."""
|
||||
return self.get_remote_app_data('admin-user-id')
|
||||
|
||||
@property
|
||||
def service_domain_name(self) -> str:
|
||||
"""Return the service_domain_name."""
|
||||
return self.get_remote_app_data('service-domain-name')
|
||||
|
||||
@property
|
||||
def service_domain_id(self) -> str:
|
||||
"""Return the service_domain_id."""
|
||||
return self.get_remote_app_data('service-domain-id')
|
||||
|
||||
@property
|
||||
def service_host(self) -> str:
|
||||
"""Return the service_host."""
|
||||
return self.get_remote_app_data('service-host')
|
||||
|
||||
@property
|
||||
def service_credentials(self) -> str:
|
||||
"""Return the service_credentials secret."""
|
||||
return self.get_remote_app_data('service-credentials')
|
||||
|
||||
@property
|
||||
def service_password(self) -> str:
|
||||
"""Return the service_password."""
|
||||
credentials_id = self.get_remote_app_data('service-credentials')
|
||||
if not credentials_id:
|
||||
return None
|
||||
|
||||
try:
|
||||
credentials = self.charm.model.get_secret(id=credentials_id)
|
||||
return credentials.get_content().get("password")
|
||||
except SecretNotFoundError:
|
||||
logger.warning(f"Secret {credentials_id} not found")
|
||||
return None
|
||||
|
||||
@property
|
||||
def service_port(self) -> str:
|
||||
"""Return the service_port."""
|
||||
return self.get_remote_app_data('service-port')
|
||||
|
||||
@property
|
||||
def service_protocol(self) -> str:
|
||||
"""Return the service_protocol."""
|
||||
return self.get_remote_app_data('service-protocol')
|
||||
|
||||
@property
|
||||
def service_project_name(self) -> str:
|
||||
"""Return the service_project_name."""
|
||||
return self.get_remote_app_data('service-project-name')
|
||||
|
||||
@property
|
||||
def service_project_id(self) -> str:
|
||||
"""Return the service_project_id."""
|
||||
return self.get_remote_app_data('service-project-id')
|
||||
|
||||
@property
|
||||
def service_user_name(self) -> str:
|
||||
"""Return the service_user_name."""
|
||||
credentials_id = self.get_remote_app_data('service-credentials')
|
||||
if not credentials_id:
|
||||
return None
|
||||
|
||||
try:
|
||||
credentials = self.charm.model.get_secret(id=credentials_id)
|
||||
return credentials.get_content().get("username")
|
||||
except SecretNotFoundError:
|
||||
logger.warning(f"Secret {credentials_id} not found")
|
||||
return None
|
||||
|
||||
@property
|
||||
def service_user_id(self) -> str:
|
||||
"""Return the service_user_id."""
|
||||
return self.get_remote_app_data('service-user-id')
|
||||
|
||||
@property
|
||||
def internal_auth_url(self) -> str:
|
||||
"""Return the internal_auth_url."""
|
||||
return self.get_remote_app_data('internal-auth-url')
|
||||
|
||||
@property
|
||||
def admin_auth_url(self) -> str:
|
||||
"""Return the admin_auth_url."""
|
||||
return self.get_remote_app_data('admin-auth-url')
|
||||
|
||||
@property
|
||||
def public_auth_url(self) -> str:
|
||||
"""Return the public_auth_url."""
|
||||
return self.get_remote_app_data('public-auth-url')
|
||||
|
||||
@property
|
||||
def admin_role(self) -> str:
|
||||
"""Return the admin_role."""
|
||||
return self.get_remote_app_data('admin-role')
|
||||
|
||||
def register_services(self, service_endpoints: dict,
|
||||
region: str) -> None:
|
||||
"""Request access to the IdentityService server."""
|
||||
if self.model.unit.is_leader():
|
||||
logging.debug("Requesting service registration")
|
||||
app_data = self._identity_service_rel.data[self.charm.app]
|
||||
app_data["service-endpoints"] = json.dumps(
|
||||
service_endpoints, sort_keys=True
|
||||
)
|
||||
app_data["region"] = region
|
||||
|
||||
|
||||
class HasIdentityServiceClientsEvent(EventBase):
|
||||
"""Has IdentityServiceClients Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ReadyIdentityServiceClientsEvent(EventBase):
|
||||
"""IdentityServiceClients Ready Event."""
|
||||
|
||||
def __init__(self, handle, relation_id, relation_name, service_endpoints,
|
||||
region, client_app_name):
|
||||
super().__init__(handle)
|
||||
self.relation_id = relation_id
|
||||
self.relation_name = relation_name
|
||||
self.service_endpoints = service_endpoints
|
||||
self.region = region
|
||||
self.client_app_name = client_app_name
|
||||
|
||||
def snapshot(self):
|
||||
return {
|
||||
"relation_id": self.relation_id,
|
||||
"relation_name": self.relation_name,
|
||||
"service_endpoints": self.service_endpoints,
|
||||
"client_app_name": self.client_app_name,
|
||||
"region": self.region}
|
||||
|
||||
def restore(self, snapshot):
|
||||
super().restore(snapshot)
|
||||
self.relation_id = snapshot["relation_id"]
|
||||
self.relation_name = snapshot["relation_name"]
|
||||
self.service_endpoints = snapshot["service_endpoints"]
|
||||
self.region = snapshot["region"]
|
||||
self.client_app_name = snapshot["client_app_name"]
|
||||
|
||||
|
||||
class IdentityServiceClientEvents(ObjectEvents):
|
||||
"""Events class for `on`"""
|
||||
|
||||
has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent)
|
||||
ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent)
|
||||
|
||||
|
||||
class IdentityServiceProvides(Object):
|
||||
"""
|
||||
IdentityServiceProvides class
|
||||
"""
|
||||
|
||||
on = IdentityServiceClientEvents()
|
||||
_stored = StoredState()
|
||||
|
||||
def __init__(self, charm, relation_name):
|
||||
super().__init__(charm, relation_name)
|
||||
self.charm = charm
|
||||
self.relation_name = relation_name
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_joined,
|
||||
self._on_identity_service_relation_joined,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_changed,
|
||||
self._on_identity_service_relation_changed,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_broken,
|
||||
self._on_identity_service_relation_broken,
|
||||
)
|
||||
|
||||
def _on_identity_service_relation_joined(self, event):
|
||||
"""Handle IdentityService joined."""
|
||||
logging.debug("IdentityService on_joined")
|
||||
self.on.has_identity_service_clients.emit()
|
||||
|
||||
def _on_identity_service_relation_changed(self, event):
|
||||
"""Handle IdentityService changed."""
|
||||
logging.debug("IdentityService on_changed")
|
||||
REQUIRED_KEYS = [
|
||||
'service-endpoints',
|
||||
'region']
|
||||
|
||||
values = [
|
||||
event.relation.data[event.relation.app].get(k)
|
||||
for k in REQUIRED_KEYS
|
||||
]
|
||||
# Validate data on the relation
|
||||
if all(values):
|
||||
service_eps = json.loads(
|
||||
event.relation.data[event.relation.app]['service-endpoints'])
|
||||
self.on.ready_identity_service_clients.emit(
|
||||
event.relation.id,
|
||||
event.relation.name,
|
||||
service_eps,
|
||||
event.relation.data[event.relation.app]['region'],
|
||||
event.relation.app.name)
|
||||
|
||||
def _on_identity_service_relation_broken(self, event):
|
||||
"""Handle IdentityService broken."""
|
||||
logging.debug("IdentityServiceProvides on_departed")
|
||||
# TODO clear data on the relation
|
||||
|
||||
def set_identity_service_credentials(self, relation_name: int,
|
||||
relation_id: str,
|
||||
api_version: str,
|
||||
auth_host: str,
|
||||
auth_port: str,
|
||||
auth_protocol: str,
|
||||
internal_host: str,
|
||||
internal_port: str,
|
||||
internal_protocol: str,
|
||||
service_host: str,
|
||||
service_port: str,
|
||||
service_protocol: str,
|
||||
admin_domain: str,
|
||||
admin_project: str,
|
||||
admin_user: str,
|
||||
service_domain: str,
|
||||
service_project: str,
|
||||
service_user: str,
|
||||
internal_auth_url: str,
|
||||
admin_auth_url: str,
|
||||
public_auth_url: str,
|
||||
service_credentials: str,
|
||||
admin_role: str):
|
||||
logging.debug("Setting identity_service connection information.")
|
||||
_identity_service_rel = None
|
||||
for relation in self.framework.model.relations[relation_name]:
|
||||
if relation.id == relation_id:
|
||||
_identity_service_rel = relation
|
||||
if not _identity_service_rel:
|
||||
# Relation has disappeared so skip send of data
|
||||
return
|
||||
app_data = _identity_service_rel.data[self.charm.app]
|
||||
app_data["api-version"] = api_version
|
||||
app_data["auth-host"] = auth_host
|
||||
app_data["auth-port"] = str(auth_port)
|
||||
app_data["auth-protocol"] = auth_protocol
|
||||
app_data["internal-host"] = internal_host
|
||||
app_data["internal-port"] = str(internal_port)
|
||||
app_data["internal-protocol"] = internal_protocol
|
||||
app_data["service-host"] = service_host
|
||||
app_data["service-port"] = str(service_port)
|
||||
app_data["service-protocol"] = service_protocol
|
||||
app_data["admin-domain-name"] = admin_domain.name
|
||||
app_data["admin-domain-id"] = admin_domain.id
|
||||
app_data["admin-project-name"] = admin_project.name
|
||||
app_data["admin-project-id"] = admin_project.id
|
||||
app_data["admin-user-name"] = admin_user.name
|
||||
app_data["admin-user-id"] = admin_user.id
|
||||
app_data["service-domain-name"] = service_domain.name
|
||||
app_data["service-domain-id"] = service_domain.id
|
||||
app_data["service-project-name"] = service_project.name
|
||||
app_data["service-project-id"] = service_project.id
|
||||
app_data["service-user-id"] = service_user.id
|
||||
app_data["internal-auth-url"] = internal_auth_url
|
||||
app_data["admin-auth-url"] = admin_auth_url
|
||||
app_data["public-auth-url"] = public_auth_url
|
||||
app_data["service-credentials"] = service_credentials
|
||||
app_data["admin-role"] = admin_role
|
@ -1,211 +0,0 @@
|
||||
"""Library for the ingress relation.
|
||||
|
||||
This library contains the Requires and Provides classes for handling
|
||||
the ingress interface.
|
||||
|
||||
Import `IngressRequires` in your charm, with two required options:
|
||||
- "self" (the charm itself)
|
||||
- config_dict
|
||||
|
||||
`config_dict` accepts the following keys:
|
||||
- service-hostname (required)
|
||||
- service-name (required)
|
||||
- service-port (required)
|
||||
- additional-hostnames
|
||||
- limit-rps
|
||||
- limit-whitelist
|
||||
- max-body-size
|
||||
- path-routes
|
||||
- retry-errors
|
||||
- rewrite-enabled
|
||||
- rewrite-target
|
||||
- service-namespace
|
||||
- session-cookie-max-age
|
||||
- tls-secret-name
|
||||
|
||||
See [the config section](https://charmhub.io/nginx-ingress-integrator/configure) for descriptions
|
||||
of each, along with the required type.
|
||||
|
||||
As an example, add the following to `src/charm.py`:
|
||||
```
|
||||
from charms.nginx_ingress_integrator.v0.ingress import IngressRequires
|
||||
|
||||
# In your charm's `__init__` method.
|
||||
self.ingress = IngressRequires(self, {"service-hostname": self.config["external_hostname"],
|
||||
"service-name": self.app.name,
|
||||
"service-port": 80})
|
||||
|
||||
# In your charm's `config-changed` handler.
|
||||
self.ingress.update_config({"service-hostname": self.config["external_hostname"]})
|
||||
```
|
||||
And then add the following to `metadata.yaml`:
|
||||
```
|
||||
requires:
|
||||
ingress:
|
||||
interface: ingress
|
||||
```
|
||||
You _must_ register the IngressRequires class as part of the `__init__` method
|
||||
rather than, for instance, a config-changed event handler. This is because
|
||||
doing so won't get the current relation changed event, because it wasn't
|
||||
registered to handle the event (because it wasn't created in `__init__` when
|
||||
the event was fired).
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from ops.charm import CharmEvents
|
||||
from ops.framework import EventBase, EventSource, Object
|
||||
from ops.model import BlockedStatus
|
||||
|
||||
# The unique Charmhub library identifier, never change it
|
||||
LIBID = "db0af4367506491c91663468fb5caa4c"
|
||||
|
||||
# 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 = 9
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
REQUIRED_INGRESS_RELATION_FIELDS = {
|
||||
"service-hostname",
|
||||
"service-name",
|
||||
"service-port",
|
||||
}
|
||||
|
||||
OPTIONAL_INGRESS_RELATION_FIELDS = {
|
||||
"additional-hostnames",
|
||||
"limit-rps",
|
||||
"limit-whitelist",
|
||||
"max-body-size",
|
||||
"retry-errors",
|
||||
"rewrite-target",
|
||||
"rewrite-enabled",
|
||||
"service-namespace",
|
||||
"session-cookie-max-age",
|
||||
"tls-secret-name",
|
||||
"path-routes",
|
||||
}
|
||||
|
||||
|
||||
class IngressAvailableEvent(EventBase):
|
||||
pass
|
||||
|
||||
|
||||
class IngressCharmEvents(CharmEvents):
|
||||
"""Custom charm events."""
|
||||
|
||||
ingress_available = EventSource(IngressAvailableEvent)
|
||||
|
||||
|
||||
class IngressRequires(Object):
|
||||
"""This class defines the functionality for the 'requires' side of the 'ingress' relation.
|
||||
|
||||
Hook events observed:
|
||||
- relation-changed
|
||||
"""
|
||||
|
||||
def __init__(self, charm, config_dict):
|
||||
super().__init__(charm, "ingress")
|
||||
|
||||
self.framework.observe(charm.on["ingress"].relation_changed, self._on_relation_changed)
|
||||
|
||||
self.config_dict = config_dict
|
||||
|
||||
def _config_dict_errors(self, update_only=False):
|
||||
"""Check our config dict for errors."""
|
||||
blocked_message = "Error in ingress relation, check `juju debug-log`"
|
||||
unknown = [
|
||||
x
|
||||
for x in self.config_dict
|
||||
if x not in REQUIRED_INGRESS_RELATION_FIELDS | OPTIONAL_INGRESS_RELATION_FIELDS
|
||||
]
|
||||
if unknown:
|
||||
logger.error(
|
||||
"Ingress relation error, unknown key(s) in config dictionary found: %s",
|
||||
", ".join(unknown),
|
||||
)
|
||||
self.model.unit.status = BlockedStatus(blocked_message)
|
||||
return True
|
||||
if not update_only:
|
||||
missing = [x for x in REQUIRED_INGRESS_RELATION_FIELDS if x not in self.config_dict]
|
||||
if missing:
|
||||
logger.error(
|
||||
"Ingress relation error, missing required key(s) in config dictionary: %s",
|
||||
", ".join(missing),
|
||||
)
|
||||
self.model.unit.status = BlockedStatus(blocked_message)
|
||||
return True
|
||||
return False
|
||||
|
||||
def _on_relation_changed(self, event):
|
||||
"""Handle the relation-changed event."""
|
||||
# `self.unit` isn't available here, so use `self.model.unit`.
|
||||
if self.model.unit.is_leader():
|
||||
if self._config_dict_errors():
|
||||
return
|
||||
for key in self.config_dict:
|
||||
event.relation.data[self.model.app][key] = str(self.config_dict[key])
|
||||
|
||||
def update_config(self, config_dict):
|
||||
"""Allow for updates to relation."""
|
||||
if self.model.unit.is_leader():
|
||||
self.config_dict = config_dict
|
||||
if self._config_dict_errors(update_only=True):
|
||||
return
|
||||
relation = self.model.get_relation("ingress")
|
||||
if relation:
|
||||
for key in self.config_dict:
|
||||
relation.data[self.model.app][key] = str(self.config_dict[key])
|
||||
|
||||
|
||||
class IngressProvides(Object):
|
||||
"""This class defines the functionality for the 'provides' side of the 'ingress' relation.
|
||||
|
||||
Hook events observed:
|
||||
- relation-changed
|
||||
"""
|
||||
|
||||
def __init__(self, charm):
|
||||
super().__init__(charm, "ingress")
|
||||
# Observe the relation-changed hook event and bind
|
||||
# self.on_relation_changed() to handle the event.
|
||||
self.framework.observe(charm.on["ingress"].relation_changed, self._on_relation_changed)
|
||||
self.charm = charm
|
||||
|
||||
def _on_relation_changed(self, event):
|
||||
"""Handle a change to the ingress relation.
|
||||
|
||||
Confirm we have the fields we expect to receive."""
|
||||
# `self.unit` isn't available here, so use `self.model.unit`.
|
||||
if not self.model.unit.is_leader():
|
||||
return
|
||||
|
||||
ingress_data = {
|
||||
field: event.relation.data[event.app].get(field)
|
||||
for field in REQUIRED_INGRESS_RELATION_FIELDS | OPTIONAL_INGRESS_RELATION_FIELDS
|
||||
}
|
||||
|
||||
missing_fields = sorted(
|
||||
[
|
||||
field
|
||||
for field in REQUIRED_INGRESS_RELATION_FIELDS
|
||||
if ingress_data.get(field) is None
|
||||
]
|
||||
)
|
||||
|
||||
if missing_fields:
|
||||
logger.error(
|
||||
"Missing required data fields for ingress relation: {}".format(
|
||||
", ".join(missing_fields)
|
||||
)
|
||||
)
|
||||
self.model.unit.status = BlockedStatus(
|
||||
"Missing fields for ingress: {}".format(", ".join(missing_fields))
|
||||
)
|
||||
|
||||
# Create an event that our charm can use to decide it's okay to
|
||||
# configure the ingress.
|
||||
self.charm.on.ingress_available.emit()
|
@ -1,286 +0,0 @@
|
||||
"""RabbitMQProvides and Requires module.
|
||||
|
||||
This library contains the Requires and Provides classes for handling
|
||||
the rabbitmq interface.
|
||||
|
||||
Import `RabbitMQRequires` in your charm, with the charm object and the
|
||||
relation name:
|
||||
- self
|
||||
- "amqp"
|
||||
|
||||
Also provide two additional parameters to the charm object:
|
||||
- username
|
||||
- vhost
|
||||
|
||||
Two events are also available to respond to:
|
||||
- connected
|
||||
- ready
|
||||
- goneaway
|
||||
|
||||
A basic example showing the usage of this relation follows:
|
||||
|
||||
```
|
||||
from charms.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires
|
||||
|
||||
class RabbitMQClientCharm(CharmBase):
|
||||
def __init__(self, *args):
|
||||
super().__init__(*args)
|
||||
# RabbitMQ Requires
|
||||
self.amqp = RabbitMQRequires(
|
||||
self, "amqp",
|
||||
username="myusername",
|
||||
vhost="vhostname"
|
||||
)
|
||||
self.framework.observe(
|
||||
self.amqp.on.connected, self._on_amqp_connected)
|
||||
self.framework.observe(
|
||||
self.amqp.on.ready, self._on_amqp_ready)
|
||||
self.framework.observe(
|
||||
self.amqp.on.goneaway, self._on_amqp_goneaway)
|
||||
|
||||
def _on_amqp_connected(self, event):
|
||||
'''React to the RabbitMQ connected event.
|
||||
|
||||
This event happens when n RabbitMQ relation is added to the
|
||||
model before credentials etc have been provided.
|
||||
'''
|
||||
# Do something before the relation is complete
|
||||
pass
|
||||
|
||||
def _on_amqp_ready(self, event):
|
||||
'''React to the RabbitMQ ready event.
|
||||
|
||||
The RabbitMQ interface will use the provided username and vhost for the
|
||||
request to the rabbitmq server.
|
||||
'''
|
||||
# RabbitMQ Relation is ready. Do something with the completed relation.
|
||||
pass
|
||||
|
||||
def _on_amqp_goneaway(self, event):
|
||||
'''React to the RabbitMQ goneaway event.
|
||||
|
||||
This event happens when an RabbitMQ relation is removed.
|
||||
'''
|
||||
# RabbitMQ Relation has goneaway. shutdown services or suchlike
|
||||
pass
|
||||
```
|
||||
"""
|
||||
|
||||
# The unique Charmhub library identifier, never change it
|
||||
LIBID = "45622352791142fd9cf87232e3bd6f2a"
|
||||
|
||||
# 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
|
||||
|
||||
import logging
|
||||
|
||||
from ops.framework import (
|
||||
StoredState,
|
||||
EventBase,
|
||||
ObjectEvents,
|
||||
EventSource,
|
||||
Object,
|
||||
)
|
||||
|
||||
from ops.model import Relation
|
||||
|
||||
from typing import List
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RabbitMQConnectedEvent(EventBase):
|
||||
"""RabbitMQ connected Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class RabbitMQReadyEvent(EventBase):
|
||||
"""RabbitMQ ready for use Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class RabbitMQGoneAwayEvent(EventBase):
|
||||
"""RabbitMQ relation has gone-away Event"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class RabbitMQServerEvents(ObjectEvents):
|
||||
"""Events class for `on`"""
|
||||
|
||||
connected = EventSource(RabbitMQConnectedEvent)
|
||||
ready = EventSource(RabbitMQReadyEvent)
|
||||
goneaway = EventSource(RabbitMQGoneAwayEvent)
|
||||
|
||||
|
||||
class RabbitMQRequires(Object):
|
||||
"""
|
||||
RabbitMQRequires class
|
||||
"""
|
||||
|
||||
on = RabbitMQServerEvents()
|
||||
|
||||
def __init__(self, charm, relation_name: str, username: str, vhost: str):
|
||||
super().__init__(charm, relation_name)
|
||||
self.charm = charm
|
||||
self.relation_name = relation_name
|
||||
self.username = username
|
||||
self.vhost = vhost
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_joined,
|
||||
self._on_amqp_relation_joined,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_changed,
|
||||
self._on_amqp_relation_changed,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_departed,
|
||||
self._on_amqp_relation_changed,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_broken,
|
||||
self._on_amqp_relation_broken,
|
||||
)
|
||||
|
||||
def _on_amqp_relation_joined(self, event):
|
||||
"""RabbitMQ relation joined."""
|
||||
logging.debug("RabbitMQRabbitMQRequires on_joined")
|
||||
self.on.connected.emit()
|
||||
self.request_access(self.username, self.vhost)
|
||||
|
||||
def _on_amqp_relation_changed(self, event):
|
||||
"""RabbitMQ relation changed."""
|
||||
logging.debug("RabbitMQRabbitMQRequires on_changed/departed")
|
||||
if self.password:
|
||||
self.on.ready.emit()
|
||||
|
||||
def _on_amqp_relation_broken(self, event):
|
||||
"""RabbitMQ relation broken."""
|
||||
logging.debug("RabbitMQRabbitMQRequires on_broken")
|
||||
self.on.goneaway.emit()
|
||||
|
||||
@property
|
||||
def _amqp_rel(self) -> Relation:
|
||||
"""The RabbitMQ relation."""
|
||||
return self.framework.model.get_relation(self.relation_name)
|
||||
|
||||
@property
|
||||
def password(self) -> str:
|
||||
"""Return the RabbitMQ password from the server side of the relation."""
|
||||
return self._amqp_rel.data[self._amqp_rel.app].get("password")
|
||||
|
||||
@property
|
||||
def hostname(self) -> str:
|
||||
"""Return the hostname from the RabbitMQ relation"""
|
||||
return self._amqp_rel.data[self._amqp_rel.app].get("hostname")
|
||||
|
||||
@property
|
||||
def ssl_port(self) -> str:
|
||||
"""Return the SSL port from the RabbitMQ relation"""
|
||||
return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port")
|
||||
|
||||
@property
|
||||
def ssl_ca(self) -> str:
|
||||
"""Return the SSL port from the RabbitMQ relation"""
|
||||
return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca")
|
||||
|
||||
@property
|
||||
def hostnames(self) -> List[str]:
|
||||
"""Return a list of remote RMQ hosts from the RabbitMQ relation"""
|
||||
_hosts = []
|
||||
for unit in self._amqp_rel.units:
|
||||
_hosts.append(self._amqp_rel.data[unit].get("ingress-address"))
|
||||
return _hosts
|
||||
|
||||
def request_access(self, username: str, vhost: str) -> None:
|
||||
"""Request access to the RabbitMQ server."""
|
||||
if self.model.unit.is_leader():
|
||||
logging.debug("Requesting RabbitMQ user and vhost")
|
||||
self._amqp_rel.data[self.charm.app]["username"] = username
|
||||
self._amqp_rel.data[self.charm.app]["vhost"] = vhost
|
||||
|
||||
|
||||
class HasRabbitMQClientsEvent(EventBase):
|
||||
"""Has RabbitMQClients Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ReadyRabbitMQClientsEvent(EventBase):
|
||||
"""RabbitMQClients Ready Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class RabbitMQClientEvents(ObjectEvents):
|
||||
"""Events class for `on`"""
|
||||
|
||||
has_amqp_clients = EventSource(HasRabbitMQClientsEvent)
|
||||
ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent)
|
||||
|
||||
|
||||
class RabbitMQProvides(Object):
|
||||
"""
|
||||
RabbitMQProvides class
|
||||
"""
|
||||
|
||||
on = RabbitMQClientEvents()
|
||||
|
||||
def __init__(self, charm, relation_name, callback):
|
||||
super().__init__(charm, relation_name)
|
||||
self.charm = charm
|
||||
self.relation_name = relation_name
|
||||
self.callback = callback
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_joined,
|
||||
self._on_amqp_relation_joined,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_changed,
|
||||
self._on_amqp_relation_changed,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_broken,
|
||||
self._on_amqp_relation_broken,
|
||||
)
|
||||
|
||||
def _on_amqp_relation_joined(self, event):
|
||||
"""Handle RabbitMQ joined."""
|
||||
logging.debug("RabbitMQRabbitMQProvides on_joined data={}"
|
||||
.format(event.relation.data[event.relation.app]))
|
||||
self.on.has_amqp_clients.emit()
|
||||
|
||||
def _on_amqp_relation_changed(self, event):
|
||||
"""Handle RabbitMQ changed."""
|
||||
logging.debug("RabbitMQRabbitMQProvides on_changed data={}"
|
||||
.format(event.relation.data[event.relation.app]))
|
||||
# Validate data on the relation
|
||||
if self.username(event) and self.vhost(event):
|
||||
self.on.ready_amqp_clients.emit()
|
||||
if self.charm.unit.is_leader():
|
||||
self.callback(event, self.username(event), self.vhost(event))
|
||||
else:
|
||||
logging.warning("Received RabbitMQ changed event without the "
|
||||
"expected keys ('username', 'vhost') in the "
|
||||
"application data bag. Incompatible charm in "
|
||||
"other end of relation?")
|
||||
|
||||
def _on_amqp_relation_broken(self, event):
|
||||
"""Handle RabbitMQ broken."""
|
||||
logging.debug("RabbitMQRabbitMQProvides on_departed")
|
||||
# TODO clear data on the relation
|
||||
|
||||
def username(self, event):
|
||||
"""Return the RabbitMQ username from the client side of the relation."""
|
||||
return event.relation.data[event.relation.app].get("username")
|
||||
|
||||
def vhost(self, event):
|
||||
"""Return the RabbitMQ vhost from the client side of the relation."""
|
||||
return event.relation.data[event.relation.app].get("vhost")
|
@ -1,734 +0,0 @@
|
||||
# Copyright 2023 Canonical Ltd.
|
||||
# See LICENSE file for licensing details.
|
||||
|
||||
r"""# Interface Library for ingress.
|
||||
|
||||
This library wraps relation endpoints using the `ingress` interface
|
||||
and provides a Python API for both requesting and providing per-application
|
||||
ingress, with load-balancing occurring across all units.
|
||||
|
||||
## Getting Started
|
||||
|
||||
To get started using the library, you just need to fetch the library using `charmcraft`.
|
||||
|
||||
```shell
|
||||
cd some-charm
|
||||
charmcraft fetch-lib charms.traefik_k8s.v1.ingress
|
||||
```
|
||||
|
||||
In the `metadata.yaml` of the charm, add the following:
|
||||
|
||||
```yaml
|
||||
requires:
|
||||
ingress:
|
||||
interface: ingress
|
||||
limit: 1
|
||||
```
|
||||
|
||||
Then, to initialise the library:
|
||||
|
||||
```python
|
||||
from charms.traefik_k8s.v2.ingress import (IngressPerAppRequirer,
|
||||
IngressPerAppReadyEvent, IngressPerAppRevokedEvent)
|
||||
|
||||
class SomeCharm(CharmBase):
|
||||
def __init__(self, *args):
|
||||
# ...
|
||||
self.ingress = IngressPerAppRequirer(self, port=80)
|
||||
# The following event is triggered when the ingress URL to be used
|
||||
# by this deployment of the `SomeCharm` is ready (or changes).
|
||||
self.framework.observe(
|
||||
self.ingress.on.ready, self._on_ingress_ready
|
||||
)
|
||||
self.framework.observe(
|
||||
self.ingress.on.revoked, self._on_ingress_revoked
|
||||
)
|
||||
|
||||
def _on_ingress_ready(self, event: IngressPerAppReadyEvent):
|
||||
logger.info("This app's ingress URL: %s", event.url)
|
||||
|
||||
def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent):
|
||||
logger.info("This app no longer has ingress")
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import socket
|
||||
import typing
|
||||
from dataclasses import dataclass
|
||||
from typing import (
|
||||
Any,
|
||||
Dict,
|
||||
List,
|
||||
MutableMapping,
|
||||
Optional,
|
||||
Sequence,
|
||||
Tuple,
|
||||
)
|
||||
|
||||
import pydantic
|
||||
from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent
|
||||
from ops.framework import EventSource, Object, ObjectEvents, StoredState
|
||||
from ops.model import ModelError, Relation, Unit
|
||||
from pydantic import AnyHttpUrl, BaseModel, Field, validator
|
||||
|
||||
# The unique Charmhub library identifier, never change it
|
||||
LIBID = "e6de2a5cd5b34422a204668f3b8f90d2"
|
||||
|
||||
# Increment this major API version when introducing breaking changes
|
||||
LIBAPI = 2
|
||||
|
||||
# Increment this PATCH version before using `charmcraft publish-lib` or reset
|
||||
# to 0 if you are raising the major API version
|
||||
LIBPATCH = 6
|
||||
|
||||
PYDEPS = ["pydantic<2.0"]
|
||||
|
||||
DEFAULT_RELATION_NAME = "ingress"
|
||||
RELATION_INTERFACE = "ingress"
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"}
|
||||
|
||||
|
||||
class DatabagModel(BaseModel):
|
||||
"""Base databag model."""
|
||||
|
||||
class Config:
|
||||
"""Pydantic config."""
|
||||
|
||||
allow_population_by_field_name = True
|
||||
"""Allow instantiating this class by field name (instead of forcing alias)."""
|
||||
|
||||
_NEST_UNDER = None
|
||||
|
||||
@classmethod
|
||||
def load(cls, databag: MutableMapping):
|
||||
"""Load this model from a Juju databag."""
|
||||
if cls._NEST_UNDER:
|
||||
return cls.parse_obj(json.loads(databag[cls._NEST_UNDER]))
|
||||
|
||||
try:
|
||||
data = {k: json.loads(v) for k, v in databag.items() if k not in BUILTIN_JUJU_KEYS}
|
||||
except json.JSONDecodeError as e:
|
||||
msg = f"invalid databag contents: expecting json. {databag}"
|
||||
log.error(msg)
|
||||
raise DataValidationError(msg) from e
|
||||
|
||||
try:
|
||||
return cls.parse_raw(json.dumps(data)) # type: ignore
|
||||
except pydantic.ValidationError as e:
|
||||
msg = f"failed to validate databag: {databag}"
|
||||
log.error(msg, exc_info=True)
|
||||
raise DataValidationError(msg) from e
|
||||
|
||||
def dump(self, databag: Optional[MutableMapping] = None, clear: bool = True):
|
||||
"""Write the contents of this model to Juju databag.
|
||||
|
||||
:param databag: the databag to write the data to.
|
||||
:param clear: ensure the databag is cleared before writing it.
|
||||
"""
|
||||
if clear and databag:
|
||||
databag.clear()
|
||||
|
||||
if databag is None:
|
||||
databag = {}
|
||||
|
||||
if self._NEST_UNDER:
|
||||
databag[self._NEST_UNDER] = self.json()
|
||||
|
||||
dct = self.dict()
|
||||
for key, field in self.__fields__.items(): # type: ignore
|
||||
value = dct[key]
|
||||
databag[field.alias or key] = json.dumps(value)
|
||||
|
||||
return databag
|
||||
|
||||
|
||||
# todo: import these models from charm-relation-interfaces/ingress/v2 instead of redeclaring them
|
||||
class IngressUrl(BaseModel):
|
||||
"""Ingress url schema."""
|
||||
|
||||
url: AnyHttpUrl
|
||||
|
||||
|
||||
class IngressProviderAppData(DatabagModel):
|
||||
"""Ingress application databag schema."""
|
||||
|
||||
ingress: IngressUrl
|
||||
|
||||
|
||||
class ProviderSchema(BaseModel):
|
||||
"""Provider schema for Ingress."""
|
||||
|
||||
app: IngressProviderAppData
|
||||
|
||||
|
||||
class IngressRequirerAppData(DatabagModel):
|
||||
"""Ingress requirer application databag model."""
|
||||
|
||||
model: str = Field(description="The model the application is in.")
|
||||
name: str = Field(description="the name of the app requesting ingress.")
|
||||
port: int = Field(description="The port the app wishes to be exposed.")
|
||||
|
||||
# fields on top of vanilla 'ingress' interface:
|
||||
strip_prefix: Optional[bool] = Field(
|
||||
description="Whether to strip the prefix from the ingress url.", alias="strip-prefix"
|
||||
)
|
||||
redirect_https: Optional[bool] = Field(
|
||||
description="Whether to redirect http traffic to https.", alias="redirect-https"
|
||||
)
|
||||
|
||||
scheme: Optional[str] = Field(
|
||||
default="http", description="What scheme to use in the generated ingress url"
|
||||
)
|
||||
|
||||
@validator("scheme", pre=True)
|
||||
def validate_scheme(cls, scheme): # noqa: N805 # pydantic wants 'cls' as first arg
|
||||
"""Validate scheme arg."""
|
||||
if scheme not in {"http", "https", "h2c"}:
|
||||
raise ValueError("invalid scheme: should be one of `http|https|h2c`")
|
||||
return scheme
|
||||
|
||||
@validator("port", pre=True)
|
||||
def validate_port(cls, port): # noqa: N805 # pydantic wants 'cls' as first arg
|
||||
"""Validate port."""
|
||||
assert isinstance(port, int), type(port)
|
||||
assert 0 < port < 65535, "port out of TCP range"
|
||||
return port
|
||||
|
||||
|
||||
class IngressRequirerUnitData(DatabagModel):
|
||||
"""Ingress requirer unit databag model."""
|
||||
|
||||
host: str = Field(description="Hostname the unit wishes to be exposed.")
|
||||
|
||||
@validator("host", pre=True)
|
||||
def validate_host(cls, host): # noqa: N805 # pydantic wants 'cls' as first arg
|
||||
"""Validate host."""
|
||||
assert isinstance(host, str), type(host)
|
||||
return host
|
||||
|
||||
|
||||
class RequirerSchema(BaseModel):
|
||||
"""Requirer schema for Ingress."""
|
||||
|
||||
app: IngressRequirerAppData
|
||||
unit: IngressRequirerUnitData
|
||||
|
||||
|
||||
class IngressError(RuntimeError):
|
||||
"""Base class for custom errors raised by this library."""
|
||||
|
||||
|
||||
class NotReadyError(IngressError):
|
||||
"""Raised when a relation is not ready."""
|
||||
|
||||
|
||||
class DataValidationError(IngressError):
|
||||
"""Raised when data validation fails on IPU relation data."""
|
||||
|
||||
|
||||
class _IngressPerAppBase(Object):
|
||||
"""Base class for IngressPerUnit interface classes."""
|
||||
|
||||
def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME):
|
||||
super().__init__(charm, relation_name)
|
||||
|
||||
self.charm: CharmBase = charm
|
||||
self.relation_name = relation_name
|
||||
self.app = self.charm.app
|
||||
self.unit = self.charm.unit
|
||||
|
||||
observe = self.framework.observe
|
||||
rel_events = charm.on[relation_name]
|
||||
observe(rel_events.relation_created, self._handle_relation)
|
||||
observe(rel_events.relation_joined, self._handle_relation)
|
||||
observe(rel_events.relation_changed, self._handle_relation)
|
||||
observe(rel_events.relation_broken, self._handle_relation_broken)
|
||||
observe(charm.on.leader_elected, self._handle_upgrade_or_leader) # type: ignore
|
||||
observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader) # type: ignore
|
||||
|
||||
@property
|
||||
def relations(self):
|
||||
"""The list of Relation instances associated with this endpoint."""
|
||||
return list(self.charm.model.relations[self.relation_name])
|
||||
|
||||
def _handle_relation(self, event):
|
||||
"""Subclasses should implement this method to handle a relation update."""
|
||||
pass
|
||||
|
||||
def _handle_relation_broken(self, event):
|
||||
"""Subclasses should implement this method to handle a relation breaking."""
|
||||
pass
|
||||
|
||||
def _handle_upgrade_or_leader(self, event):
|
||||
"""Subclasses should implement this method to handle upgrades or leadership change."""
|
||||
pass
|
||||
|
||||
|
||||
class _IPAEvent(RelationEvent):
|
||||
__args__: Tuple[str, ...] = ()
|
||||
__optional_kwargs__: Dict[str, Any] = {}
|
||||
|
||||
@classmethod
|
||||
def __attrs__(cls):
|
||||
return cls.__args__ + tuple(cls.__optional_kwargs__.keys())
|
||||
|
||||
def __init__(self, handle, relation, *args, **kwargs):
|
||||
super().__init__(handle, relation)
|
||||
|
||||
if not len(self.__args__) == len(args):
|
||||
raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args)))
|
||||
|
||||
for attr, obj in zip(self.__args__, args):
|
||||
setattr(self, attr, obj)
|
||||
for attr, default in self.__optional_kwargs__.items():
|
||||
obj = kwargs.get(attr, default)
|
||||
setattr(self, attr, obj)
|
||||
|
||||
def snapshot(self):
|
||||
dct = super().snapshot()
|
||||
for attr in self.__attrs__():
|
||||
obj = getattr(self, attr)
|
||||
try:
|
||||
dct[attr] = obj
|
||||
except ValueError as e:
|
||||
raise ValueError(
|
||||
"cannot automagically serialize {}: "
|
||||
"override this method and do it "
|
||||
"manually.".format(obj)
|
||||
) from e
|
||||
|
||||
return dct
|
||||
|
||||
def restore(self, snapshot) -> None:
|
||||
super().restore(snapshot)
|
||||
for attr, obj in snapshot.items():
|
||||
setattr(self, attr, obj)
|
||||
|
||||
|
||||
class IngressPerAppDataProvidedEvent(_IPAEvent):
|
||||
"""Event representing that ingress data has been provided for an app."""
|
||||
|
||||
__args__ = ("name", "model", "hosts", "strip_prefix", "redirect_https")
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
name: Optional[str] = None
|
||||
model: Optional[str] = None
|
||||
# sequence of hostname, port dicts
|
||||
hosts: Sequence["IngressRequirerUnitData"] = ()
|
||||
strip_prefix: bool = False
|
||||
redirect_https: bool = False
|
||||
|
||||
|
||||
class IngressPerAppDataRemovedEvent(RelationEvent):
|
||||
"""Event representing that ingress data has been removed for an app."""
|
||||
|
||||
|
||||
class IngressPerAppProviderEvents(ObjectEvents):
|
||||
"""Container for IPA Provider events."""
|
||||
|
||||
data_provided = EventSource(IngressPerAppDataProvidedEvent)
|
||||
data_removed = EventSource(IngressPerAppDataRemovedEvent)
|
||||
|
||||
|
||||
@dataclass
|
||||
class IngressRequirerData:
|
||||
"""Data exposed by the ingress requirer to the provider."""
|
||||
|
||||
app: "IngressRequirerAppData"
|
||||
units: List["IngressRequirerUnitData"]
|
||||
|
||||
|
||||
class TlsProviderType(typing.Protocol):
|
||||
"""Placeholder."""
|
||||
|
||||
@property
|
||||
def enabled(self) -> bool: # type: ignore
|
||||
"""Placeholder."""
|
||||
|
||||
|
||||
class IngressPerAppProvider(_IngressPerAppBase):
|
||||
"""Implementation of the provider of ingress."""
|
||||
|
||||
on = IngressPerAppProviderEvents() # type: ignore
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
charm: CharmBase,
|
||||
relation_name: str = DEFAULT_RELATION_NAME,
|
||||
):
|
||||
"""Constructor for IngressPerAppProvider.
|
||||
|
||||
Args:
|
||||
charm: The charm that is instantiating the instance.
|
||||
relation_name: The name of the relation endpoint to bind to
|
||||
(defaults to "ingress").
|
||||
"""
|
||||
super().__init__(charm, relation_name)
|
||||
|
||||
def _handle_relation(self, event):
|
||||
# created, joined or changed: if remote side has sent the required data:
|
||||
# notify listeners.
|
||||
if self.is_ready(event.relation):
|
||||
data = self.get_data(event.relation)
|
||||
self.on.data_provided.emit( # type: ignore
|
||||
event.relation,
|
||||
data.app.name,
|
||||
data.app.model,
|
||||
[unit.dict() for unit in data.units],
|
||||
data.app.strip_prefix or False,
|
||||
data.app.redirect_https or False,
|
||||
)
|
||||
|
||||
def _handle_relation_broken(self, event):
|
||||
self.on.data_removed.emit(event.relation) # type: ignore
|
||||
|
||||
def wipe_ingress_data(self, relation: Relation):
|
||||
"""Clear ingress data from relation."""
|
||||
assert self.unit.is_leader(), "only leaders can do this"
|
||||
try:
|
||||
relation.data
|
||||
except ModelError as e:
|
||||
log.warning(
|
||||
"error {} accessing relation data for {!r}. "
|
||||
"Probably a ghost of a dead relation is still "
|
||||
"lingering around.".format(e, relation.name)
|
||||
)
|
||||
return
|
||||
del relation.data[self.app]["ingress"]
|
||||
|
||||
def _get_requirer_units_data(self, relation: Relation) -> List["IngressRequirerUnitData"]:
|
||||
"""Fetch and validate the requirer's app databag."""
|
||||
out: List["IngressRequirerUnitData"] = []
|
||||
|
||||
unit: Unit
|
||||
for unit in relation.units:
|
||||
databag = relation.data[unit]
|
||||
try:
|
||||
data = IngressRequirerUnitData.load(databag)
|
||||
out.append(data)
|
||||
except pydantic.ValidationError:
|
||||
log.info(f"failed to validate remote unit data for {unit}")
|
||||
raise
|
||||
return out
|
||||
|
||||
@staticmethod
|
||||
def _get_requirer_app_data(relation: Relation) -> "IngressRequirerAppData":
|
||||
"""Fetch and validate the requirer's app databag."""
|
||||
app = relation.app
|
||||
if app is None:
|
||||
raise NotReadyError(relation)
|
||||
|
||||
databag = relation.data[app]
|
||||
return IngressRequirerAppData.load(databag)
|
||||
|
||||
def get_data(self, relation: Relation) -> IngressRequirerData:
|
||||
"""Fetch the remote (requirer) app and units' databags."""
|
||||
try:
|
||||
return IngressRequirerData(
|
||||
self._get_requirer_app_data(relation), self._get_requirer_units_data(relation)
|
||||
)
|
||||
except (pydantic.ValidationError, DataValidationError) as e:
|
||||
raise DataValidationError("failed to validate ingress requirer data") from e
|
||||
|
||||
def is_ready(self, relation: Optional[Relation] = None):
|
||||
"""The Provider is ready if the requirer has sent valid data."""
|
||||
if not relation:
|
||||
return any(map(self.is_ready, self.relations))
|
||||
|
||||
try:
|
||||
self.get_data(relation)
|
||||
except (DataValidationError, NotReadyError) as e:
|
||||
log.debug("Provider not ready; validation error encountered: %s" % str(e))
|
||||
return False
|
||||
return True
|
||||
|
||||
def _published_url(self, relation: Relation) -> Optional["IngressProviderAppData"]:
|
||||
"""Fetch and validate this app databag; return the ingress url."""
|
||||
if not self.is_ready(relation) or not self.unit.is_leader():
|
||||
# Handle edge case where remote app name can be missing, e.g.,
|
||||
# relation_broken events.
|
||||
# Also, only leader units can read own app databags.
|
||||
# FIXME https://github.com/canonical/traefik-k8s-operator/issues/34
|
||||
return None
|
||||
|
||||
# fetch the provider's app databag
|
||||
databag = relation.data[self.app]
|
||||
if not databag.get("ingress"):
|
||||
raise NotReadyError("This application did not `publish_url` yet.")
|
||||
|
||||
return IngressProviderAppData.load(databag)
|
||||
|
||||
def publish_url(self, relation: Relation, url: str):
|
||||
"""Publish to the app databag the ingress url."""
|
||||
ingress_url = {"url": url}
|
||||
IngressProviderAppData.parse_obj({"ingress": ingress_url}).dump(relation.data[self.app])
|
||||
|
||||
@property
|
||||
def proxied_endpoints(self) -> Dict[str, str]:
|
||||
"""Returns the ingress settings provided to applications by this IngressPerAppProvider.
|
||||
|
||||
For example, when this IngressPerAppProvider has provided the
|
||||
`http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary
|
||||
will be:
|
||||
|
||||
```
|
||||
{
|
||||
"my-app": {
|
||||
"url": "http://foo.bar/my-model.my-app"
|
||||
}
|
||||
}
|
||||
```
|
||||
"""
|
||||
results = {}
|
||||
|
||||
for ingress_relation in self.relations:
|
||||
if not ingress_relation.app:
|
||||
log.warning(
|
||||
f"no app in relation {ingress_relation} when fetching proxied endpoints: skipping"
|
||||
)
|
||||
continue
|
||||
try:
|
||||
ingress_data = self._published_url(ingress_relation)
|
||||
except NotReadyError:
|
||||
log.warning(
|
||||
f"no published url found in {ingress_relation}: "
|
||||
f"traefik didn't publish_url yet to this relation."
|
||||
)
|
||||
continue
|
||||
|
||||
if not ingress_data:
|
||||
log.warning(f"relation {ingress_relation} not ready yet: try again in some time.")
|
||||
continue
|
||||
|
||||
results[ingress_relation.app.name] = ingress_data.ingress.dict()
|
||||
return results
|
||||
|
||||
|
||||
class IngressPerAppReadyEvent(_IPAEvent):
|
||||
"""Event representing that ingress for an app is ready."""
|
||||
|
||||
__args__ = ("url",)
|
||||
if typing.TYPE_CHECKING:
|
||||
url: Optional[str] = None
|
||||
|
||||
|
||||
class IngressPerAppRevokedEvent(RelationEvent):
|
||||
"""Event representing that ingress for an app has been revoked."""
|
||||
|
||||
|
||||
class IngressPerAppRequirerEvents(ObjectEvents):
|
||||
"""Container for IPA Requirer events."""
|
||||
|
||||
ready = EventSource(IngressPerAppReadyEvent)
|
||||
revoked = EventSource(IngressPerAppRevokedEvent)
|
||||
|
||||
|
||||
class IngressPerAppRequirer(_IngressPerAppBase):
|
||||
"""Implementation of the requirer of the ingress relation."""
|
||||
|
||||
on = IngressPerAppRequirerEvents() # type: ignore
|
||||
|
||||
# used to prevent spurious urls to be sent out if the event we're currently
|
||||
# handling is a relation-broken one.
|
||||
_stored = StoredState()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
charm: CharmBase,
|
||||
relation_name: str = DEFAULT_RELATION_NAME,
|
||||
*,
|
||||
host: Optional[str] = None,
|
||||
port: Optional[int] = None,
|
||||
strip_prefix: bool = False,
|
||||
redirect_https: bool = False,
|
||||
# fixme: this is horrible UX.
|
||||
# shall we switch to manually calling provide_ingress_requirements with all args when ready?
|
||||
scheme: typing.Callable[[], str] = lambda: "http",
|
||||
):
|
||||
"""Constructor for IngressRequirer.
|
||||
|
||||
The request args can be used to specify the ingress properties when the
|
||||
instance is created. If any are set, at least `port` is required, and
|
||||
they will be sent to the ingress provider as soon as it is available.
|
||||
All request args must be given as keyword args.
|
||||
|
||||
Args:
|
||||
charm: the charm that is instantiating the library.
|
||||
relation_name: the name of the relation endpoint to bind to (defaults to `ingress`);
|
||||
relation must be of interface type `ingress` and have "limit: 1")
|
||||
host: Hostname to be used by the ingress provider to address the requiring
|
||||
application; if unspecified, the default Kubernetes service name will be used.
|
||||
strip_prefix: configure Traefik to strip the path prefix.
|
||||
redirect_https: redirect incoming requests to HTTPS.
|
||||
scheme: callable returning the scheme to use when constructing the ingress url.
|
||||
|
||||
Request Args:
|
||||
port: the port of the service
|
||||
"""
|
||||
super().__init__(charm, relation_name)
|
||||
self.charm: CharmBase = charm
|
||||
self.relation_name = relation_name
|
||||
self._strip_prefix = strip_prefix
|
||||
self._redirect_https = redirect_https
|
||||
self._get_scheme = scheme
|
||||
|
||||
self._stored.set_default(current_url=None) # type: ignore
|
||||
|
||||
# if instantiated with a port, and we are related, then
|
||||
# we immediately publish our ingress data to speed up the process.
|
||||
if port:
|
||||
self._auto_data = host, port
|
||||
else:
|
||||
self._auto_data = None
|
||||
|
||||
def _handle_relation(self, event):
|
||||
# created, joined or changed: if we have auto data: publish it
|
||||
self._publish_auto_data()
|
||||
|
||||
if self.is_ready():
|
||||
# Avoid spurious events, emit only when there is a NEW URL available
|
||||
new_url = (
|
||||
None
|
||||
if isinstance(event, RelationBrokenEvent)
|
||||
else self._get_url_from_relation_data()
|
||||
)
|
||||
if self._stored.current_url != new_url: # type: ignore
|
||||
self._stored.current_url = new_url # type: ignore
|
||||
self.on.ready.emit(event.relation, new_url) # type: ignore
|
||||
|
||||
def _handle_relation_broken(self, event):
|
||||
self._stored.current_url = None # type: ignore
|
||||
self.on.revoked.emit(event.relation) # type: ignore
|
||||
|
||||
def _handle_upgrade_or_leader(self, event):
|
||||
"""On upgrade/leadership change: ensure we publish the data we have."""
|
||||
self._publish_auto_data()
|
||||
|
||||
def is_ready(self):
|
||||
"""The Requirer is ready if the Provider has sent valid data."""
|
||||
try:
|
||||
return bool(self._get_url_from_relation_data())
|
||||
except DataValidationError as e:
|
||||
log.debug("Requirer not ready; validation error encountered: %s" % str(e))
|
||||
return False
|
||||
|
||||
def _publish_auto_data(self):
|
||||
if self._auto_data:
|
||||
host, port = self._auto_data
|
||||
self.provide_ingress_requirements(host=host, port=port)
|
||||
|
||||
def provide_ingress_requirements(
|
||||
self,
|
||||
*,
|
||||
scheme: Optional[str] = None,
|
||||
host: Optional[str] = None,
|
||||
port: int,
|
||||
):
|
||||
"""Publishes the data that Traefik needs to provide ingress.
|
||||
|
||||
Args:
|
||||
scheme: Scheme to be used; if unspecified, use the one used by __init__.
|
||||
host: Hostname to be used by the ingress provider to address the
|
||||
requirer unit; if unspecified, FQDN will be used instead
|
||||
port: the port of the service (required)
|
||||
"""
|
||||
for relation in self.relations:
|
||||
self._provide_ingress_requirements(scheme, host, port, relation)
|
||||
|
||||
def _provide_ingress_requirements(
|
||||
self,
|
||||
scheme: Optional[str],
|
||||
host: Optional[str],
|
||||
port: int,
|
||||
relation: Relation,
|
||||
):
|
||||
if self.unit.is_leader():
|
||||
self._publish_app_data(scheme, port, relation)
|
||||
|
||||
self._publish_unit_data(host, relation)
|
||||
|
||||
def _publish_unit_data(
|
||||
self,
|
||||
host: Optional[str],
|
||||
relation: Relation,
|
||||
):
|
||||
if not host:
|
||||
host = socket.getfqdn()
|
||||
|
||||
unit_databag = relation.data[self.unit]
|
||||
try:
|
||||
IngressRequirerUnitData(host=host).dump(unit_databag)
|
||||
except pydantic.ValidationError as e:
|
||||
msg = "failed to validate unit data"
|
||||
log.info(msg, exc_info=True) # log to INFO because this might be expected
|
||||
raise DataValidationError(msg) from e
|
||||
|
||||
def _publish_app_data(
|
||||
self,
|
||||
scheme: Optional[str],
|
||||
port: int,
|
||||
relation: Relation,
|
||||
):
|
||||
# assumes leadership!
|
||||
app_databag = relation.data[self.app]
|
||||
|
||||
if not scheme:
|
||||
# If scheme was not provided, use the one given to the constructor.
|
||||
scheme = self._get_scheme()
|
||||
|
||||
try:
|
||||
IngressRequirerAppData( # type: ignore # pyright does not like aliases
|
||||
model=self.model.name,
|
||||
name=self.app.name,
|
||||
scheme=scheme,
|
||||
port=port,
|
||||
strip_prefix=self._strip_prefix, # type: ignore # pyright does not like aliases
|
||||
redirect_https=self._redirect_https, # type: ignore # pyright does not like aliases
|
||||
).dump(app_databag)
|
||||
except pydantic.ValidationError as e:
|
||||
msg = "failed to validate app data"
|
||||
log.info(msg, exc_info=True) # log to INFO because this might be expected
|
||||
raise DataValidationError(msg) from e
|
||||
|
||||
@property
|
||||
def relation(self):
|
||||
"""The established Relation instance, or None."""
|
||||
return self.relations[0] if self.relations else None
|
||||
|
||||
def _get_url_from_relation_data(self) -> Optional[str]:
|
||||
"""The full ingress URL to reach the current unit.
|
||||
|
||||
Returns None if the URL isn't available yet.
|
||||
"""
|
||||
relation = self.relation
|
||||
if not relation or not relation.app:
|
||||
return None
|
||||
|
||||
# fetch the provider's app databag
|
||||
try:
|
||||
databag = relation.data[relation.app]
|
||||
except ModelError as e:
|
||||
log.debug(
|
||||
f"Error {e} attempting to read remote app data; "
|
||||
f"probably we are in a relation_departed hook"
|
||||
)
|
||||
return None
|
||||
|
||||
if not databag: # not ready yet
|
||||
return None
|
||||
|
||||
return str(IngressProviderAppData.load(databag).ingress.url)
|
||||
|
||||
@property
|
||||
def url(self) -> Optional[str]:
|
||||
"""The full ingress URL to reach the current unit.
|
||||
|
||||
Returns None if the URL isn't available yet.
|
||||
"""
|
||||
data = (
|
||||
typing.cast(Optional[str], self._stored.current_url) # type: ignore
|
||||
or self._get_url_from_relation_data()
|
||||
)
|
||||
return data
|
@ -1,10 +0,0 @@
|
||||
- project:
|
||||
templates:
|
||||
- charm-publish-jobs
|
||||
vars:
|
||||
needs_charm_build: true
|
||||
charm_build_name: cinder-k8s
|
||||
build_type: charmcraft
|
||||
publish_charm: true
|
||||
charmcraft_channel: 2.0/stable
|
||||
publish_channel: 2023.2/edge
|
@ -1,39 +0,0 @@
|
||||
# Copyright 2022 Canonical Ltd.
|
||||
# See LICENSE file for licensing details.
|
||||
|
||||
# Testing tools configuration
|
||||
[tool.coverage.run]
|
||||
branch = true
|
||||
|
||||
[tool.coverage.report]
|
||||
show_missing = true
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
minversion = "6.0"
|
||||
log_cli_level = "INFO"
|
||||
|
||||
# Formatting tools configuration
|
||||
[tool.black]
|
||||
line-length = 79
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
multi_line_output = 3
|
||||
force_grid_wrap = true
|
||||
|
||||
# Linting tools configuration
|
||||
[tool.flake8]
|
||||
max-line-length = 79
|
||||
max-doc-length = 99
|
||||
max-complexity = 10
|
||||
exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"]
|
||||
select = ["E", "W", "F", "C", "N", "R", "D", "H"]
|
||||
# Ignore W503, E501 because using black creates errors with this
|
||||
# Ignore D107 Missing docstring in __init__
|
||||
ignore = ["W503", "E501", "D107", "E402"]
|
||||
per-file-ignores = []
|
||||
docstring-convention = "google"
|
||||
# Check for properly formatted copyright header in each file
|
||||
copyright-check = "True"
|
||||
copyright-author = "Canonical Ltd."
|
||||
copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s"
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user