Deploy heat-api-cfn container as part of heat charm
Currently the heat charm instance runs heat-api and heat-engine services or heat-api-cfn or heat-engine services. Change heat charm to deploy heat-api, heat-api-cfn, heat-engine containers. Change the ingress relation to use traefik-route interface instead of ingress interface so that the traefik configuration for heat-api and heat-api-cfn serive can be written by charm. Add heat-api-cfn pebble container handler and update service endpoints accordingly. Remove heat-config interface and corresponding handlers. Change-Id: I391f8d4ffefcebdb2423fcc1947590ca906d711a
This commit is contained in:
parent
9609359416
commit
4a473da7fe
@ -4,19 +4,19 @@ options:
|
|||||||
description: Enable debug logging.
|
description: Enable debug logging.
|
||||||
type: boolean
|
type: boolean
|
||||||
os-admin-hostname:
|
os-admin-hostname:
|
||||||
default: glance.juju
|
default: heat.juju
|
||||||
description: |
|
description: |
|
||||||
The hostname or address of the admin endpoints that should be advertised
|
The hostname or address of the admin endpoints that should be advertised
|
||||||
in the glance image provider.
|
in the glance image provider.
|
||||||
type: string
|
type: string
|
||||||
os-internal-hostname:
|
os-internal-hostname:
|
||||||
default: glance.juju
|
default: heat.juju
|
||||||
description: |
|
description: |
|
||||||
The hostname or address of the internal endpoints that should be advertised
|
The hostname or address of the internal endpoints that should be advertised
|
||||||
in the glance image provider.
|
in the glance image provider.
|
||||||
type: string
|
type: string
|
||||||
os-public-hostname:
|
os-public-hostname:
|
||||||
default: glance.juju
|
default: heat.juju
|
||||||
description: |
|
description: |
|
||||||
The hostname or address of the internal endpoints that should be advertised
|
The hostname or address of the internal endpoints that should be advertised
|
||||||
in the glance image provider.
|
in the glance image provider.
|
||||||
@ -25,10 +25,3 @@ options:
|
|||||||
default: RegionOne
|
default: RegionOne
|
||||||
description: Space delimited list of OpenStack regions
|
description: Space delimited list of OpenStack regions
|
||||||
type: string
|
type: string
|
||||||
api_service:
|
|
||||||
default: heat-api
|
|
||||||
description: |
|
|
||||||
Value should be one of heat-api or heat-api-cfn. The configuration parameter
|
|
||||||
is only applicable during the initial deploy of the charm and change in the
|
|
||||||
configuration does not have any effect once deployed.
|
|
||||||
type: string
|
|
||||||
|
@ -5,4 +5,4 @@ charmcraft fetch-lib charms.data_platform_libs.v0.database_requires
|
|||||||
charmcraft fetch-lib charms.keystone_k8s.v0.identity_resource
|
charmcraft fetch-lib charms.keystone_k8s.v0.identity_resource
|
||||||
charmcraft fetch-lib charms.keystone_k8s.v1.identity_service
|
charmcraft fetch-lib charms.keystone_k8s.v1.identity_service
|
||||||
charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq
|
charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq
|
||||||
charmcraft fetch-lib charms.traefik_k8s.v2.ingress
|
charmcraft fetch-lib charms.traefik_route_k8s.v0.traefik_route
|
||||||
|
@ -1,209 +0,0 @@
|
|||||||
"""HeatSharedConfig Provides and Requires module.
|
|
||||||
|
|
||||||
This library contains the Requires and Provides classes for handling
|
|
||||||
the heat-shared-config interface.
|
|
||||||
|
|
||||||
Import `HeatSharedConfigRequires` in your charm, with the charm object and the
|
|
||||||
relation name:
|
|
||||||
- self
|
|
||||||
- "heat_config"
|
|
||||||
|
|
||||||
Two events are also available to respond to:
|
|
||||||
- config_changed
|
|
||||||
- goneaway
|
|
||||||
|
|
||||||
A basic example showing the usage of this relation follows:
|
|
||||||
|
|
||||||
```
|
|
||||||
from charms.heat_k8s.v0.heat_shared_config import (
|
|
||||||
HeatSharedConfigRequires
|
|
||||||
)
|
|
||||||
|
|
||||||
class HeatSharedConfigClientCharm(CharmBase):
|
|
||||||
def __init__(self, *args):
|
|
||||||
super().__init__(*args)
|
|
||||||
# HeatSharedConfig Requires
|
|
||||||
self.heat_config = HeatSharedConfigRequires(
|
|
||||||
self, "heat_config",
|
|
||||||
)
|
|
||||||
self.framework.observe(
|
|
||||||
self.heat_config.on.config_changed,
|
|
||||||
self._on_heat_shared_config_changed
|
|
||||||
)
|
|
||||||
self.framework.observe(
|
|
||||||
self.heat_config.on.goneaway,
|
|
||||||
self._on_heat_shared_config_goneaway
|
|
||||||
)
|
|
||||||
|
|
||||||
def _on_heat_shared_config_changed(self, event):
|
|
||||||
'''React to the Heat shared config changed event.
|
|
||||||
|
|
||||||
This event happens when Heat shared config relation is added to the
|
|
||||||
model and relation data is changed.
|
|
||||||
'''
|
|
||||||
# Do something with the configuration provided by relation.
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _on_heat_shared_config_goneaway(self, event):
|
|
||||||
'''React to the HeatSharedConfig goneaway event.
|
|
||||||
|
|
||||||
This event happens when Heat shared config relation is removed.
|
|
||||||
'''
|
|
||||||
# HeatSharedConfig Relation has goneaway.
|
|
||||||
pass
|
|
||||||
```
|
|
||||||
"""
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from typing import (
|
|
||||||
Optional,
|
|
||||||
)
|
|
||||||
|
|
||||||
from ops.charm import (
|
|
||||||
CharmBase,
|
|
||||||
RelationBrokenEvent,
|
|
||||||
RelationChangedEvent,
|
|
||||||
RelationEvent,
|
|
||||||
)
|
|
||||||
from ops.framework import (
|
|
||||||
EventSource,
|
|
||||||
Object,
|
|
||||||
ObjectEvents,
|
|
||||||
)
|
|
||||||
from ops.model import (
|
|
||||||
Relation,
|
|
||||||
)
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
# The unique Charmhub library identifier, never change it
|
|
||||||
LIBID = "88823d2312d34be08ba8940b3b30c3d4"
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
|
|
||||||
class HeatSharedConfigRequestEvent(RelationEvent):
|
|
||||||
"""HeatConfigRequest Event."""
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class HeatSharedConfigProviderEvents(ObjectEvents):
|
|
||||||
"""Events class for `on`."""
|
|
||||||
|
|
||||||
config_request = EventSource(HeatSharedConfigRequestEvent)
|
|
||||||
|
|
||||||
|
|
||||||
class HeatSharedConfigProvides(Object):
|
|
||||||
"""HeatSharedConfigProvides class."""
|
|
||||||
|
|
||||||
on = HeatSharedConfigProviderEvents()
|
|
||||||
|
|
||||||
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_heat_shared_config_relation_changed,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _on_heat_shared_config_relation_changed(
|
|
||||||
self, event: RelationChangedEvent
|
|
||||||
):
|
|
||||||
"""Handle HeatSharedConfig relation changed."""
|
|
||||||
logging.debug("HeatSharedConfig relation changed")
|
|
||||||
self.on.config_request.emit(event.relation)
|
|
||||||
|
|
||||||
def set_config(
|
|
||||||
self, relation: Relation, auth_encryption_key: str
|
|
||||||
) -> None:
|
|
||||||
"""Set heat configuration on the relation."""
|
|
||||||
if not self.charm.unit.is_leader():
|
|
||||||
logging.debug("Not a leader unit, skipping set config")
|
|
||||||
return
|
|
||||||
|
|
||||||
logging.debug(
|
|
||||||
f"Sending config on relation {relation.app.name} "
|
|
||||||
f"{relation.name}/{relation.id}"
|
|
||||||
)
|
|
||||||
relation.data[self.charm.app][
|
|
||||||
"auth-encryption-key"
|
|
||||||
] = auth_encryption_key
|
|
||||||
|
|
||||||
|
|
||||||
class HeatSharedConfigChangedEvent(RelationEvent):
|
|
||||||
"""HeatSharedConfigChanged Event."""
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class HeatSharedConfigGoneAwayEvent(RelationEvent):
|
|
||||||
"""HeatSharedConfigGoneAway Event."""
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class HeatSharedConfigRequirerEvents(ObjectEvents):
|
|
||||||
"""Events class for `on`."""
|
|
||||||
|
|
||||||
config_changed = EventSource(HeatSharedConfigChangedEvent)
|
|
||||||
goneaway = EventSource(HeatSharedConfigGoneAwayEvent)
|
|
||||||
|
|
||||||
|
|
||||||
class HeatSharedConfigRequires(Object):
|
|
||||||
"""HeatSharedConfigRequires class."""
|
|
||||||
|
|
||||||
on = HeatSharedConfigRequirerEvents()
|
|
||||||
|
|
||||||
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_heat_shared_config_relation_changed,
|
|
||||||
)
|
|
||||||
self.framework.observe(
|
|
||||||
self.charm.on[relation_name].relation_broken,
|
|
||||||
self._on_heat_shared_config_relation_broken,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _on_heat_shared_config_relation_changed(
|
|
||||||
self, event: RelationChangedEvent
|
|
||||||
):
|
|
||||||
"""Handle HeatSharedConfig relation changed."""
|
|
||||||
logging.debug("HeatSharedConfig config data changed")
|
|
||||||
self.on.config_changed.emit(event.relation)
|
|
||||||
|
|
||||||
def _on_heat_shared_config_relation_broken(
|
|
||||||
self, event: RelationBrokenEvent
|
|
||||||
):
|
|
||||||
"""Handle HeatSharedConfig relation changed."""
|
|
||||||
logging.debug("HeatSharedConfig on_broken")
|
|
||||||
self.on.goneaway.emit(event.relation)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _heat_shared_config_rel(self) -> Optional[Relation]:
|
|
||||||
"""The heat shared config relation."""
|
|
||||||
return self.framework.model.get_relation(self.relation_name)
|
|
||||||
|
|
||||||
def get_remote_app_data(self, key: str) -> Optional[str]:
|
|
||||||
"""Return the value for the given key from remote app data."""
|
|
||||||
if self._heat_shared_config_rel:
|
|
||||||
data = self._heat_shared_config_rel.data[
|
|
||||||
self._heat_shared_config_rel.app
|
|
||||||
]
|
|
||||||
return data.get(key)
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def auth_encryption_key(self) -> Optional[str]:
|
|
||||||
"""Return the auth_encryption_key."""
|
|
||||||
return self.get_remote_app_data("auth-encryption-key")
|
|
359
charms/heat-k8s/lib/charms/traefik_route_k8s/v0/traefik_route.py
Normal file
359
charms/heat-k8s/lib/charms/traefik_route_k8s/v0/traefik_route.py
Normal file
@ -0,0 +1,359 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# Copyright 2022 Canonical Ltd.
|
||||||
|
# See LICENSE file for licensing details.
|
||||||
|
|
||||||
|
r"""# Interface Library for traefik_route.
|
||||||
|
|
||||||
|
This library wraps relation endpoints for traefik_route. The requirer of this
|
||||||
|
relation is the traefik-route-k8s charm, or any charm capable of providing
|
||||||
|
Traefik configuration files. The provider is the traefik-k8s charm, or another
|
||||||
|
charm willing to consume Traefik configuration files.
|
||||||
|
|
||||||
|
## 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_route_k8s.v0.traefik_route
|
||||||
|
```
|
||||||
|
|
||||||
|
To use the library from the provider side (Traefik):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
requires:
|
||||||
|
traefik_route:
|
||||||
|
interface: traefik_route
|
||||||
|
limit: 1
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
from charms.traefik_route_k8s.v0.traefik_route import TraefikRouteProvider
|
||||||
|
|
||||||
|
class TraefikCharm(CharmBase):
|
||||||
|
def __init__(self, *args):
|
||||||
|
# ...
|
||||||
|
self.traefik_route = TraefikRouteProvider(self)
|
||||||
|
|
||||||
|
self.framework.observe(
|
||||||
|
self.traefik_route.on.ready, self._handle_traefik_route_ready
|
||||||
|
)
|
||||||
|
|
||||||
|
def _handle_traefik_route_ready(self, event):
|
||||||
|
config: str = self.traefik_route.get_config(event.relation) # yaml
|
||||||
|
# use config to configure Traefik
|
||||||
|
```
|
||||||
|
|
||||||
|
To use the library from the requirer side (TraefikRoute):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
requires:
|
||||||
|
traefik-route:
|
||||||
|
interface: traefik_route
|
||||||
|
limit: 1
|
||||||
|
optional: false
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ...
|
||||||
|
from charms.traefik_route_k8s.v0.traefik_route import TraefikRouteRequirer
|
||||||
|
|
||||||
|
class TraefikRouteCharm(CharmBase):
|
||||||
|
def __init__(self, *args):
|
||||||
|
# ...
|
||||||
|
traefik_route = TraefikRouteRequirer(
|
||||||
|
self, self.model.relations.get("traefik-route"),
|
||||||
|
"traefik-route"
|
||||||
|
)
|
||||||
|
if traefik_route.is_ready():
|
||||||
|
traefik_route.submit_to_traefik(
|
||||||
|
config={'my': {'traefik': 'configuration'}}
|
||||||
|
)
|
||||||
|
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
from ops.charm import CharmBase, CharmEvents, RelationEvent
|
||||||
|
from ops.framework import EventSource, Object, StoredState
|
||||||
|
from ops.model import Relation
|
||||||
|
|
||||||
|
# The unique Charmhub library identifier, never change it
|
||||||
|
LIBID = "fe2ac43a373949f2bf61383b9f35c83c"
|
||||||
|
|
||||||
|
# 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 = 8
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class TraefikRouteException(RuntimeError):
|
||||||
|
"""Base class for exceptions raised by TraefikRoute."""
|
||||||
|
|
||||||
|
|
||||||
|
class UnauthorizedError(TraefikRouteException):
|
||||||
|
"""Raised when the unit needs leadership to perform some action."""
|
||||||
|
|
||||||
|
|
||||||
|
class TraefikRouteProviderReadyEvent(RelationEvent):
|
||||||
|
"""Event emitted when Traefik is ready to provide ingress for a routed unit."""
|
||||||
|
|
||||||
|
|
||||||
|
class TraefikRouteProviderDataRemovedEvent(RelationEvent):
|
||||||
|
"""Event emitted when a routed ingress relation is removed."""
|
||||||
|
|
||||||
|
|
||||||
|
class TraefikRouteRequirerReadyEvent(RelationEvent):
|
||||||
|
"""Event emitted when a unit requesting ingress has provided all data Traefik needs."""
|
||||||
|
|
||||||
|
|
||||||
|
class TraefikRouteRequirerEvents(CharmEvents):
|
||||||
|
"""Container for TraefikRouteRequirer events."""
|
||||||
|
|
||||||
|
ready = EventSource(TraefikRouteRequirerReadyEvent)
|
||||||
|
|
||||||
|
|
||||||
|
class TraefikRouteProviderEvents(CharmEvents):
|
||||||
|
"""Container for TraefikRouteProvider events."""
|
||||||
|
|
||||||
|
ready = EventSource(TraefikRouteProviderReadyEvent) # TODO rename to data_provided in v1
|
||||||
|
data_removed = EventSource(TraefikRouteProviderDataRemovedEvent)
|
||||||
|
|
||||||
|
|
||||||
|
class TraefikRouteProvider(Object):
|
||||||
|
"""Implementation of the provider of traefik_route.
|
||||||
|
|
||||||
|
This will presumably be owned by a Traefik charm.
|
||||||
|
The main idea is that Traefik will observe the `ready` event and, upon
|
||||||
|
receiving it, will fetch the config from the TraefikRoute's application databag,
|
||||||
|
apply it, and update its own app databag to let Route know that the ingress
|
||||||
|
is there.
|
||||||
|
The TraefikRouteProvider provides api to do this easily.
|
||||||
|
"""
|
||||||
|
|
||||||
|
on = TraefikRouteProviderEvents()
|
||||||
|
_stored = StoredState()
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
charm: CharmBase,
|
||||||
|
relation_name: str = "traefik-route",
|
||||||
|
external_host: str = "",
|
||||||
|
*,
|
||||||
|
scheme: str = "http",
|
||||||
|
):
|
||||||
|
"""Constructor for TraefikRouteProvider.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
charm: The charm that is instantiating the instance.
|
||||||
|
relation_name: The name of the relation relation_name to bind to
|
||||||
|
(defaults to "traefik-route").
|
||||||
|
external_host: The external host.
|
||||||
|
scheme: The scheme.
|
||||||
|
"""
|
||||||
|
super().__init__(charm, relation_name)
|
||||||
|
self._stored.set_default(external_host=None, scheme=None)
|
||||||
|
|
||||||
|
self._charm = charm
|
||||||
|
self._relation_name = relation_name
|
||||||
|
|
||||||
|
if self._stored.external_host != external_host or self._stored.scheme != scheme:
|
||||||
|
# If traefik endpoint details changed, update
|
||||||
|
self.update_traefik_address(external_host=external_host, scheme=scheme)
|
||||||
|
|
||||||
|
self.framework.observe(
|
||||||
|
self._charm.on[relation_name].relation_changed, self._on_relation_changed
|
||||||
|
)
|
||||||
|
self.framework.observe(
|
||||||
|
self._charm.on[relation_name].relation_broken, self._on_relation_broken
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def external_host(self) -> str:
|
||||||
|
"""Return the external host set by Traefik, if any."""
|
||||||
|
self._update_stored()
|
||||||
|
return self._stored.external_host or "" # type: ignore
|
||||||
|
|
||||||
|
@property
|
||||||
|
def scheme(self) -> str:
|
||||||
|
"""Return the scheme set by Traefik, if any."""
|
||||||
|
self._update_stored()
|
||||||
|
return self._stored.scheme or "" # 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 _update_stored(self) -> None:
|
||||||
|
"""Ensure that the stored data is up-to-date.
|
||||||
|
|
||||||
|
This is split out into a separate method since, in the case of multi-unit deployments,
|
||||||
|
removal of a `TraefikRouteRequirer` will not cause a `RelationEvent`, but the guard on
|
||||||
|
app data ensures that only the previous leader will know what it is. Separating it
|
||||||
|
allows for re-use both when the property is called and if the relation changes, so a
|
||||||
|
leader change where the new leader checks the property will do the right thing.
|
||||||
|
"""
|
||||||
|
if not self._charm.unit.is_leader():
|
||||||
|
return
|
||||||
|
|
||||||
|
for relation in self._charm.model.relations[self._relation_name]:
|
||||||
|
if not relation.app:
|
||||||
|
self._stored.external_host = ""
|
||||||
|
self._stored.scheme = ""
|
||||||
|
return
|
||||||
|
external_host = relation.data[relation.app].get("external_host", "")
|
||||||
|
self._stored.external_host = external_host or self._stored.external_host
|
||||||
|
scheme = relation.data[relation.app].get("scheme", "")
|
||||||
|
self._stored.scheme = scheme or self._stored.scheme
|
||||||
|
|
||||||
|
def _on_relation_changed(self, event: RelationEvent):
|
||||||
|
if self.is_ready(event.relation):
|
||||||
|
# todo check data is valid here?
|
||||||
|
self.update_traefik_address()
|
||||||
|
self.on.ready.emit(event.relation)
|
||||||
|
|
||||||
|
def _on_relation_broken(self, event: RelationEvent):
|
||||||
|
self.on.data_removed.emit(event.relation)
|
||||||
|
|
||||||
|
def update_traefik_address(
|
||||||
|
self, *, external_host: Optional[str] = None, scheme: Optional[str] = None
|
||||||
|
):
|
||||||
|
"""Ensure that requirers know the external host for Traefik."""
|
||||||
|
if not self._charm.unit.is_leader():
|
||||||
|
return
|
||||||
|
|
||||||
|
for relation in self._charm.model.relations[self._relation_name]:
|
||||||
|
relation.data[self._charm.app]["external_host"] = external_host or self.external_host
|
||||||
|
relation.data[self._charm.app]["scheme"] = scheme or self.scheme
|
||||||
|
|
||||||
|
# We first attempt to write relation data (which may raise) and only then update stored
|
||||||
|
# state.
|
||||||
|
self._stored.external_host = external_host
|
||||||
|
self._stored.scheme = scheme
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_ready(relation: Relation) -> bool:
|
||||||
|
"""Whether TraefikRoute is ready on this relation.
|
||||||
|
|
||||||
|
Returns True when the remote app shared the config; False otherwise.
|
||||||
|
"""
|
||||||
|
assert relation.app is not None # not currently handled anyway
|
||||||
|
return "config" in relation.data[relation.app]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_config(relation: Relation) -> Optional[str]:
|
||||||
|
"""Retrieve the config published by the remote application."""
|
||||||
|
# TODO: validate this config
|
||||||
|
assert relation.app is not None # not currently handled anyway
|
||||||
|
return relation.data[relation.app].get("config")
|
||||||
|
|
||||||
|
|
||||||
|
class TraefikRouteRequirer(Object):
|
||||||
|
"""Wrapper for the requirer side of traefik-route.
|
||||||
|
|
||||||
|
The traefik_route requirer will publish to the application databag an object like:
|
||||||
|
{
|
||||||
|
'config': <Traefik_config>
|
||||||
|
}
|
||||||
|
|
||||||
|
NB: TraefikRouteRequirer does no validation; it assumes that the
|
||||||
|
traefik-route-k8s charm will provide valid yaml-encoded config.
|
||||||
|
The TraefikRouteRequirer provides api to store this config in the
|
||||||
|
application databag.
|
||||||
|
"""
|
||||||
|
|
||||||
|
on = TraefikRouteRequirerEvents()
|
||||||
|
_stored = StoredState()
|
||||||
|
|
||||||
|
def __init__(self, charm: CharmBase, relation: Relation, relation_name: str = "traefik-route"):
|
||||||
|
super(TraefikRouteRequirer, self).__init__(charm, relation_name)
|
||||||
|
self._stored.set_default(external_host=None, scheme=None)
|
||||||
|
|
||||||
|
self._charm = charm
|
||||||
|
self._relation = relation
|
||||||
|
|
||||||
|
self.framework.observe(
|
||||||
|
self._charm.on[relation_name].relation_changed, self._on_relation_changed
|
||||||
|
)
|
||||||
|
self.framework.observe(
|
||||||
|
self._charm.on[relation_name].relation_broken, self._on_relation_broken
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def external_host(self) -> str:
|
||||||
|
"""Return the external host set by Traefik, if any."""
|
||||||
|
self._update_stored()
|
||||||
|
return self._stored.external_host or "" # type: ignore
|
||||||
|
|
||||||
|
@property
|
||||||
|
def scheme(self) -> str:
|
||||||
|
"""Return the scheme set by Traefik, if any."""
|
||||||
|
self._update_stored()
|
||||||
|
return self._stored.scheme or "" # type: ignore
|
||||||
|
|
||||||
|
def _update_stored(self) -> None:
|
||||||
|
"""Ensure that the stored host is up-to-date.
|
||||||
|
|
||||||
|
This is split out into a separate method since, in the case of multi-unit deployments,
|
||||||
|
removal of a `TraefikRouteRequirer` will not cause a `RelationEvent`, but the guard on
|
||||||
|
app data ensures that only the previous leader will know what it is. Separating it
|
||||||
|
allows for re-use both when the property is called and if the relation changes, so a
|
||||||
|
leader change where the new leader checks the property will do the right thing.
|
||||||
|
"""
|
||||||
|
if not self._charm.unit.is_leader():
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._relation:
|
||||||
|
for relation in self._charm.model.relations[self._relation.name]:
|
||||||
|
if not relation.app:
|
||||||
|
self._stored.external_host = ""
|
||||||
|
self._stored.scheme = ""
|
||||||
|
return
|
||||||
|
external_host = relation.data[relation.app].get("external_host", "")
|
||||||
|
self._stored.external_host = external_host or self._stored.external_host
|
||||||
|
scheme = relation.data[relation.app].get("scheme", "")
|
||||||
|
self._stored.scheme = scheme or self._stored.scheme
|
||||||
|
|
||||||
|
def _on_relation_changed(self, event: RelationEvent) -> None:
|
||||||
|
"""Update StoredState with external_host and other information from Traefik."""
|
||||||
|
self._update_stored()
|
||||||
|
if self._charm.unit.is_leader():
|
||||||
|
self.on.ready.emit(event.relation)
|
||||||
|
|
||||||
|
def _on_relation_broken(self, event: RelationEvent) -> None:
|
||||||
|
"""On RelationBroken, clear the stored data if set and emit an event."""
|
||||||
|
self._stored.external_host = ""
|
||||||
|
if self._charm.unit.is_leader():
|
||||||
|
self.on.ready.emit(event.relation)
|
||||||
|
|
||||||
|
def is_ready(self) -> bool:
|
||||||
|
"""Is the TraefikRouteRequirer ready to submit data to Traefik?"""
|
||||||
|
if self._relation:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
# return self._relation is not None
|
||||||
|
|
||||||
|
def submit_to_traefik(self, config):
|
||||||
|
"""Relay an ingress configuration data structure to traefik.
|
||||||
|
|
||||||
|
This will publish to TraefikRoute's traefik-route relation databag
|
||||||
|
the config traefik needs to route the units behind this charm.
|
||||||
|
"""
|
||||||
|
if not self._charm.unit.is_leader():
|
||||||
|
raise UnauthorizedError()
|
||||||
|
|
||||||
|
if not self._relation:
|
||||||
|
return
|
||||||
|
|
||||||
|
app_databag = self._relation.data[self._charm.app]
|
||||||
|
|
||||||
|
# Traefik thrives on yaml, feels pointless to talk json to Route
|
||||||
|
app_databag["config"] = yaml.safe_dump(config)
|
@ -20,6 +20,8 @@ issues: https://bugs.launchpad.net/charm-heat-k8s
|
|||||||
containers:
|
containers:
|
||||||
heat-api:
|
heat-api:
|
||||||
resource: heat-api-image
|
resource: heat-api-image
|
||||||
|
heat-api-cfn:
|
||||||
|
resource: heat-api-image
|
||||||
heat-engine:
|
heat-engine:
|
||||||
resource: heat-engine-image
|
resource: heat-engine-image
|
||||||
|
|
||||||
@ -41,24 +43,17 @@ requires:
|
|||||||
limit: 1
|
limit: 1
|
||||||
identity-service:
|
identity-service:
|
||||||
interface: keystone
|
interface: keystone
|
||||||
ingress-internal:
|
traefik-route-internal:
|
||||||
interface: ingress
|
interface: traefik_route
|
||||||
optional: true
|
optional: true
|
||||||
limit: 1
|
limit: 1
|
||||||
ingress-public:
|
traefik-route-public:
|
||||||
interface: ingress
|
interface: traefik_route
|
||||||
limit: 1
|
limit: 1
|
||||||
amqp:
|
amqp:
|
||||||
interface: rabbitmq
|
interface: rabbitmq
|
||||||
identity-ops:
|
identity-ops:
|
||||||
interface: keystone-resources
|
interface: keystone-resources
|
||||||
heat-config:
|
|
||||||
interface: heat-shared-config
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
provides:
|
|
||||||
heat-service:
|
|
||||||
interface: heat-shared-config
|
|
||||||
|
|
||||||
peers:
|
peers:
|
||||||
peers:
|
peers:
|
||||||
|
@ -22,6 +22,7 @@ import hashlib
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import secrets
|
import secrets
|
||||||
|
import socket
|
||||||
from typing import (
|
from typing import (
|
||||||
Callable,
|
Callable,
|
||||||
List,
|
List,
|
||||||
@ -35,14 +36,7 @@ import ops_sunbeam.config_contexts as sunbeam_config_contexts
|
|||||||
import ops_sunbeam.container_handlers as sunbeam_chandlers
|
import ops_sunbeam.container_handlers as sunbeam_chandlers
|
||||||
import ops_sunbeam.core as sunbeam_core
|
import ops_sunbeam.core as sunbeam_core
|
||||||
import ops_sunbeam.relation_handlers as sunbeam_rhandlers
|
import ops_sunbeam.relation_handlers as sunbeam_rhandlers
|
||||||
from charms.heat_k8s.v0.heat_shared_config import (
|
|
||||||
HeatSharedConfigChangedEvent,
|
|
||||||
HeatSharedConfigProvides,
|
|
||||||
HeatSharedConfigRequestEvent,
|
|
||||||
HeatSharedConfigRequires,
|
|
||||||
)
|
|
||||||
from ops.charm import (
|
from ops.charm import (
|
||||||
CharmBase,
|
|
||||||
RelationEvent,
|
RelationEvent,
|
||||||
)
|
)
|
||||||
from ops.framework import (
|
from ops.framework import (
|
||||||
@ -53,130 +47,74 @@ from ops.main import (
|
|||||||
)
|
)
|
||||||
from ops.model import (
|
from ops.model import (
|
||||||
ModelError,
|
ModelError,
|
||||||
Relation,
|
|
||||||
SecretNotFoundError,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
CREDENTIALS_SECRET_PREFIX = "credentials_"
|
|
||||||
HEAT_API_CONTAINER = "heat-api"
|
HEAT_API_CONTAINER = "heat-api"
|
||||||
|
HEAT_API_CFN_CONTAINER = "heat-api-cfn"
|
||||||
HEAT_ENGINE_CONTAINER = "heat-engine"
|
HEAT_ENGINE_CONTAINER = "heat-engine"
|
||||||
HEAT_API_SERVICE_KEY = "api-service"
|
HEAT_API_INGRESS_NAME = "heat"
|
||||||
HEAT_API_SERVICE_NAME = "heat-api"
|
HEAT_API_CFN_INGRESS_NAME = "heat-cfn"
|
||||||
HEAT_CFN_SERVICE_NAME = "heat-api-cfn"
|
HEAT_API_PORT = 8004
|
||||||
|
HEAT_API_CFN_PORT = 8000
|
||||||
|
|
||||||
|
|
||||||
class HeatSharedConfigProvidesHandler(sunbeam_rhandlers.RelationHandler):
|
class TraefikRouteHandler(sunbeam_rhandlers.RelationHandler):
|
||||||
"""Handler for heat shared config relation on provider side."""
|
"""Base class to handle traefik route relations."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
charm: CharmBase,
|
charm: ops.charm.CharmBase,
|
||||||
relation_name: str,
|
|
||||||
callback_f: Callable,
|
|
||||||
):
|
|
||||||
"""Create a new heat-shared-config handler.
|
|
||||||
|
|
||||||
Create a new HeatSharedConfigProvidesHandler that updates heat config
|
|
||||||
auth-encryption-key on the related units.
|
|
||||||
|
|
||||||
:param charm: the Charm class the handler is for
|
|
||||||
:type charm: ops.charm.CharmBase
|
|
||||||
:param relation_name: the relation the handler is bound to
|
|
||||||
:type relation_name: str
|
|
||||||
:param callback_f: the function to call when the nodes are connected
|
|
||||||
:type callback_f: Callable
|
|
||||||
"""
|
|
||||||
super().__init__(charm, relation_name, callback_f)
|
|
||||||
|
|
||||||
def setup_event_handler(self):
|
|
||||||
"""Configure event handlers for Heat shared config relation."""
|
|
||||||
logger.debug("Setting up Heat shared config event handler")
|
|
||||||
svc = HeatSharedConfigProvides(
|
|
||||||
self.charm,
|
|
||||||
self.relation_name,
|
|
||||||
)
|
|
||||||
self.framework.observe(
|
|
||||||
svc.on.config_request,
|
|
||||||
self._on_config_request,
|
|
||||||
)
|
|
||||||
return svc
|
|
||||||
|
|
||||||
def _on_config_request(self, event: HeatSharedConfigRequestEvent) -> None:
|
|
||||||
"""Handle Config request event."""
|
|
||||||
self.callback_f(event)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def ready(self) -> bool:
|
|
||||||
"""Report if relation is ready."""
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class HeatSharedConfigRequiresHandler(sunbeam_rhandlers.RelationHandler):
|
|
||||||
"""Handle heat shared config relation on the requires side."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
charm: CharmBase,
|
|
||||||
relation_name: str,
|
relation_name: str,
|
||||||
callback_f: Callable,
|
callback_f: Callable,
|
||||||
mandatory: bool = False,
|
mandatory: bool = False,
|
||||||
):
|
) -> None:
|
||||||
"""Create a new heat-shared-config handler.
|
"""Run constructor."""
|
||||||
|
|
||||||
Create a new HeatSharedConfigRequiresHandler that handles initial
|
|
||||||
events from the relation and invokes the provided callbacks based on
|
|
||||||
the event raised.
|
|
||||||
|
|
||||||
:param charm: the Charm class the handler is for
|
|
||||||
:type charm: ops.charm.CharmBase
|
|
||||||
:param relation_name: the relation the handler is bound to
|
|
||||||
:type relation_name: str
|
|
||||||
:param callback_f: the function to call when the nodes are connected
|
|
||||||
:type callback_f: Callable
|
|
||||||
:param mandatory: If the relation is mandatory to proceed with
|
|
||||||
configuring charm
|
|
||||||
:type mandatory: bool
|
|
||||||
"""
|
|
||||||
super().__init__(charm, relation_name, callback_f, mandatory)
|
super().__init__(charm, relation_name, callback_f, mandatory)
|
||||||
|
|
||||||
def setup_event_handler(self) -> None:
|
def setup_event_handler(self) -> ops.charm.Object:
|
||||||
"""Configure event handlers for Heat shared config relation."""
|
"""Configure event handlers for an Ingress relation."""
|
||||||
logger.debug("Setting up Heat shared config event handler")
|
logger.debug("Setting up ingress event handler")
|
||||||
svc = HeatSharedConfigRequires(
|
from charms.traefik_route_k8s.v0.traefik_route import (
|
||||||
|
TraefikRouteRequirer,
|
||||||
|
)
|
||||||
|
|
||||||
|
interface = TraefikRouteRequirer(
|
||||||
self.charm,
|
self.charm,
|
||||||
|
self.model.get_relation(self.relation_name),
|
||||||
self.relation_name,
|
self.relation_name,
|
||||||
)
|
)
|
||||||
self.framework.observe(
|
|
||||||
svc.on.config_changed,
|
|
||||||
self._on_config_changed,
|
|
||||||
)
|
|
||||||
self.framework.observe(
|
|
||||||
svc.on.goneaway,
|
|
||||||
self._on_goneaway,
|
|
||||||
)
|
|
||||||
return svc
|
|
||||||
|
|
||||||
def _on_config_changed(self, event: RelationEvent) -> None:
|
self.framework.observe(interface.on.ready, self._on_ingress_ready)
|
||||||
"""Handle config_changed event."""
|
self.framework.observe(
|
||||||
logger.debug(
|
self.charm.on[self.relation_name].relation_joined,
|
||||||
"Heat shared config provider config changed event received"
|
self._on_traefik_relation_joined,
|
||||||
)
|
)
|
||||||
self.callback_f(event)
|
return interface
|
||||||
|
|
||||||
def _on_goneaway(self, event: RelationEvent) -> None:
|
def _on_traefik_relation_joined(self, event: RelationEvent) -> None:
|
||||||
"""Handle gone_away event."""
|
"""Handle traefik relation joined event."""
|
||||||
logger.debug("Heat shared config relation is departed/broken")
|
# This is passed as None during the init method, so update the
|
||||||
self.callback_f(event)
|
# relation attribute in TraefikRouteRequirer
|
||||||
|
self.interface._relation = event.relation
|
||||||
|
|
||||||
|
def _on_ingress_ready(self, event: RelationEvent) -> None:
|
||||||
|
"""Handle ingress relation changed events.
|
||||||
|
|
||||||
|
`event` is an instance of
|
||||||
|
`charms.traefik_k8s.v2.ingress.IngressPerAppReadyEvent`.
|
||||||
|
"""
|
||||||
|
if self.interface.is_ready():
|
||||||
|
self.callback_f(event)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ready(self) -> bool:
|
def ready(self) -> bool:
|
||||||
"""Whether handler is ready for use."""
|
"""Whether the handler is ready for use."""
|
||||||
try:
|
if self.charm.unit.is_leader():
|
||||||
return bool(self.interface.auth_encryption_key)
|
return bool(self.interface.external_host)
|
||||||
except (AttributeError, KeyError):
|
else:
|
||||||
return False
|
return self.interface.is_ready()
|
||||||
|
|
||||||
|
|
||||||
class HeatAPIPebbleHandler(sunbeam_chandlers.ServicePebbleHandler):
|
class HeatAPIPebbleHandler(sunbeam_chandlers.ServicePebbleHandler):
|
||||||
@ -188,36 +126,20 @@ class HeatAPIPebbleHandler(sunbeam_chandlers.ServicePebbleHandler):
|
|||||||
:returns: pebble service layer configuration for heat api service
|
:returns: pebble service layer configuration for heat api service
|
||||||
:rtype: dict
|
:rtype: dict
|
||||||
"""
|
"""
|
||||||
if self.charm.service_name == HEAT_CFN_SERVICE_NAME:
|
return {
|
||||||
return {
|
"summary": "heat api layer",
|
||||||
"summary": "heat api cfn layer",
|
"description": "pebble configuration for heat api service",
|
||||||
"description": "pebble configuration for heat api cfn service",
|
"services": {
|
||||||
"services": {
|
"heat-api": {
|
||||||
"heat-api": {
|
"override": "replace",
|
||||||
"override": "replace",
|
"summary": "Heat API",
|
||||||
"summary": "Heat API CFN",
|
"command": "heat-api",
|
||||||
"command": "heat-api-cfn",
|
"startup": "enabled",
|
||||||
"startup": "enabled",
|
"user": "heat",
|
||||||
"user": "heat",
|
"group": "heat",
|
||||||
"group": "heat",
|
}
|
||||||
}
|
},
|
||||||
},
|
}
|
||||||
}
|
|
||||||
else:
|
|
||||||
return {
|
|
||||||
"summary": "heat api layer",
|
|
||||||
"description": "pebble configuration for heat api service",
|
|
||||||
"services": {
|
|
||||||
"heat-api": {
|
|
||||||
"override": "replace",
|
|
||||||
"summary": "Heat API",
|
|
||||||
"command": "heat-api",
|
|
||||||
"startup": "enabled",
|
|
||||||
"user": "heat",
|
|
||||||
"group": "heat",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_healthcheck_layer(self) -> dict:
|
def get_healthcheck_layer(self) -> dict:
|
||||||
"""Health check pebble layer.
|
"""Health check pebble layer.
|
||||||
@ -230,7 +152,49 @@ class HeatAPIPebbleHandler(sunbeam_chandlers.ServicePebbleHandler):
|
|||||||
"override": "replace",
|
"override": "replace",
|
||||||
"level": "ready",
|
"level": "ready",
|
||||||
"http": {
|
"http": {
|
||||||
"url": f"{self.charm.healthcheck_http_url}/healthcheck"
|
"url": f"http://localhost:{HEAT_API_PORT}/healthcheck"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class HeatCfnAPIPebbleHandler(sunbeam_chandlers.ServicePebbleHandler):
|
||||||
|
"""Pebble handler for Heat CFN API container."""
|
||||||
|
|
||||||
|
def get_layer(self):
|
||||||
|
"""Heat CFN API service.
|
||||||
|
|
||||||
|
:returns: pebble service layer configuration for heat cfn api service
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"summary": "heat api cfn layer",
|
||||||
|
"description": "pebble configuration for heat api cfn service",
|
||||||
|
"services": {
|
||||||
|
"heat-api-cfn": {
|
||||||
|
"override": "replace",
|
||||||
|
"summary": "Heat API CFN",
|
||||||
|
"command": "heat-api-cfn --config-file /etc/heat/heat-api-cfn.conf",
|
||||||
|
"startup": "enabled",
|
||||||
|
"user": "heat",
|
||||||
|
"group": "heat",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_healthcheck_layer(self) -> dict:
|
||||||
|
"""Health check pebble layer.
|
||||||
|
|
||||||
|
:returns: pebble health check layer configuration for heat service
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"checks": {
|
||||||
|
"online": {
|
||||||
|
"override": "replace",
|
||||||
|
"level": "ready",
|
||||||
|
"http": {
|
||||||
|
"url": f"http://localhost:{HEAT_API_CFN_PORT}/healthcheck"
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -285,6 +249,8 @@ class HeatConfigurationContext(sunbeam_config_contexts.ConfigContext):
|
|||||||
"stack_domain_admin_user": username,
|
"stack_domain_admin_user": username,
|
||||||
"stack_domain_admin_password": password,
|
"stack_domain_admin_password": password,
|
||||||
"auth_encryption_key": self.charm.get_heat_auth_encryption_key(),
|
"auth_encryption_key": self.charm.get_heat_auth_encryption_key(),
|
||||||
|
"ingress_path": f"/{self.charm.model.name}-{HEAT_API_INGRESS_NAME}",
|
||||||
|
"cfn_ingress_path": f"/{self.charm.model.name}-{HEAT_API_CFN_INGRESS_NAME}",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -302,13 +268,28 @@ class HeatOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
|||||||
"database",
|
"database",
|
||||||
"amqp",
|
"amqp",
|
||||||
"identity-service",
|
"identity-service",
|
||||||
"ingress-public",
|
"traefik-route-public",
|
||||||
"identity-ops",
|
"identity-ops",
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, framework):
|
def __init__(self, framework):
|
||||||
|
self.traefik_route_public = None
|
||||||
|
self.traefik_route_internal = None
|
||||||
super().__init__(framework)
|
super().__init__(framework)
|
||||||
self._state.set_default(identity_ops_ready=False)
|
self._state.set_default(identity_ops_ready=False)
|
||||||
|
self.framework.observe(
|
||||||
|
self.on.peers_relation_created, self._on_peer_relation_created
|
||||||
|
)
|
||||||
|
self.framework.observe(
|
||||||
|
self.on["peers"].relation_departed, self._on_peer_relation_departed
|
||||||
|
)
|
||||||
|
|
||||||
|
def _on_peer_relation_created(self, event: ops.EventBase) -> None:
|
||||||
|
logger.info("Setting peer unit data")
|
||||||
|
self.peers.set_unit_data({"host": socket.getfqdn()})
|
||||||
|
|
||||||
|
def _on_peer_relation_departed(self, event: ops.EventBase) -> None:
|
||||||
|
self.handle_traefik_ready(event)
|
||||||
|
|
||||||
def hash_ops(self, ops: list) -> str:
|
def hash_ops(self, ops: list) -> str:
|
||||||
"""Return the sha1 of the requested ops."""
|
"""Return the sha1 of the requested ops."""
|
||||||
@ -333,26 +314,20 @@ class HeatOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
|||||||
)
|
)
|
||||||
handlers.append(self.user_id_ops)
|
handlers.append(self.user_id_ops)
|
||||||
|
|
||||||
if self.service_name == HEAT_CFN_SERVICE_NAME:
|
self.traefik_route_public = TraefikRouteHandler(
|
||||||
# heat-config is not a mandatory relation.
|
self,
|
||||||
# If instance of heat-k8s deployed with service heat-api-cfn and
|
"traefik-route-public",
|
||||||
# without any heat-api, heat-api-cfn workload should still come
|
self.handle_traefik_ready,
|
||||||
# to active using internally generated auth-encryption-key
|
"traefik-route-public" in self.mandatory_relations,
|
||||||
self.heat_config_receiver = HeatSharedConfigRequiresHandler(
|
)
|
||||||
self,
|
handlers.append(self.traefik_route_public)
|
||||||
"heat-config",
|
self.traefik_route_internal = TraefikRouteHandler(
|
||||||
self.handle_heat_config_events,
|
self,
|
||||||
"heat-config" in self.mandatory_relations,
|
"traefik-route-internal",
|
||||||
)
|
self.handle_traefik_ready,
|
||||||
handlers.append(self.heat_config_receiver)
|
"traefik-route-internal" in self.mandatory_relations,
|
||||||
else:
|
)
|
||||||
self.config_svc = HeatSharedConfigProvidesHandler(
|
handlers.append(self.traefik_route_internal)
|
||||||
self,
|
|
||||||
"heat-service",
|
|
||||||
self.set_config_from_event,
|
|
||||||
)
|
|
||||||
handlers.append(self.config_svc)
|
|
||||||
|
|
||||||
return handlers
|
return handlers
|
||||||
|
|
||||||
def get_pebble_handlers(
|
def get_pebble_handlers(
|
||||||
@ -364,7 +339,15 @@ class HeatOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
|||||||
self,
|
self,
|
||||||
HEAT_API_CONTAINER,
|
HEAT_API_CONTAINER,
|
||||||
"heat-api",
|
"heat-api",
|
||||||
self.default_container_configs(),
|
self.heat_api_container_configs(),
|
||||||
|
self.template_dir,
|
||||||
|
self.configure_charm,
|
||||||
|
),
|
||||||
|
HeatCfnAPIPebbleHandler(
|
||||||
|
self,
|
||||||
|
HEAT_API_CFN_CONTAINER,
|
||||||
|
"heat-api-cfn",
|
||||||
|
self.heat_api_cfn_container_configs(),
|
||||||
self.template_dir,
|
self.template_dir,
|
||||||
self.configure_charm,
|
self.configure_charm,
|
||||||
),
|
),
|
||||||
@ -372,7 +355,7 @@ class HeatOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
|||||||
self,
|
self,
|
||||||
HEAT_ENGINE_CONTAINER,
|
HEAT_ENGINE_CONTAINER,
|
||||||
"heat-engine",
|
"heat-engine",
|
||||||
self.default_container_configs(),
|
self.heat_api_container_configs(),
|
||||||
self.template_dir,
|
self.template_dir,
|
||||||
self.configure_charm,
|
self.configure_charm,
|
||||||
),
|
),
|
||||||
@ -423,23 +406,96 @@ class HeatOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
|||||||
else:
|
else:
|
||||||
logger.debug("Creating auth key")
|
logger.debug("Creating auth key")
|
||||||
self.set_heat_auth_encryption_key()
|
self.set_heat_auth_encryption_key()
|
||||||
if self.service_name == HEAT_API_SERVICE_NAME:
|
|
||||||
# Send Auth encryption key over heat-service relation
|
self.handle_traefik_ready(event)
|
||||||
self.set_config_on_update()
|
|
||||||
|
|
||||||
super().configure_charm(event)
|
super().configure_charm(event)
|
||||||
|
|
||||||
def configure_app_leader(self, event):
|
@property
|
||||||
"""Configure app leader.
|
def traefik_config(self) -> dict:
|
||||||
|
"""Config to publish to traefik."""
|
||||||
|
model = self.model.name
|
||||||
|
router_cfg = {}
|
||||||
|
# Add routers for both heat-api and heat-api-cfn
|
||||||
|
for app in HEAT_API_INGRESS_NAME, HEAT_API_CFN_INGRESS_NAME:
|
||||||
|
router_cfg.update(
|
||||||
|
{
|
||||||
|
f"juju-{model}-{app}-router": {
|
||||||
|
"rule": f"PathPrefix(`/{model}-{app}`)",
|
||||||
|
"service": f"juju-{model}-{app}-service",
|
||||||
|
"entryPoints": ["web"],
|
||||||
|
},
|
||||||
|
f"juju-{model}-{app}-router-tls": {
|
||||||
|
"rule": f"PathPrefix(`/{model}-{app}`)",
|
||||||
|
"service": f"juju-{model}-{app}-service",
|
||||||
|
"entryPoints": ["websecure"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
Ensure setting service_name in peer relation application data if it
|
# Get host key value from all units
|
||||||
does not exist.
|
hosts = self.peers.get_all_unit_values(
|
||||||
"""
|
key="host", include_local_unit=True
|
||||||
super().configure_app_leader(event)
|
)
|
||||||
|
api_lb_servers = [
|
||||||
|
{"url": f"http://{host}:{HEAT_API_PORT}"} for host in hosts
|
||||||
|
]
|
||||||
|
cfn_lb_servers = [
|
||||||
|
{"url": f"http://{host}:{HEAT_API_CFN_PORT}"} for host in hosts
|
||||||
|
]
|
||||||
|
# Add services for heat-api and heat-api-cfn
|
||||||
|
service_cfg = {
|
||||||
|
f"juju-{model}-{HEAT_API_INGRESS_NAME}-service": {
|
||||||
|
"loadBalancer": {"servers": api_lb_servers},
|
||||||
|
},
|
||||||
|
f"juju-{model}-{HEAT_API_CFN_INGRESS_NAME}-service": {
|
||||||
|
"loadBalancer": {"servers": cfn_lb_servers},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
# Update service name in application data
|
config = {
|
||||||
if not self.leader_get(HEAT_API_SERVICE_KEY):
|
"http": {
|
||||||
self.leader_set({HEAT_API_SERVICE_KEY: self.service_name})
|
"routers": router_cfg,
|
||||||
|
"services": service_cfg,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
|
||||||
|
def _update_service_endpoints(self):
|
||||||
|
try:
|
||||||
|
if self.id_svc.update_service_endpoints:
|
||||||
|
logger.info(
|
||||||
|
"Updating service endpoints after ingress relation changed"
|
||||||
|
)
|
||||||
|
self.id_svc.update_service_endpoints(self.service_endpoints)
|
||||||
|
except (AttributeError, KeyError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def handle_traefik_ready(self, event: ops.EventBase):
|
||||||
|
"""Handle Traefik route ready callback."""
|
||||||
|
if not self.unit.is_leader():
|
||||||
|
logger.debug(
|
||||||
|
"Not a leader unit, not updating traefik route config"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.traefik_route_public:
|
||||||
|
logger.debug("Sending traefik config for public interface")
|
||||||
|
self.traefik_route_public.interface.submit_to_traefik(
|
||||||
|
config=self.traefik_config
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.traefik_route_public.ready:
|
||||||
|
self._update_service_endpoints()
|
||||||
|
|
||||||
|
if self.traefik_route_internal:
|
||||||
|
logger.debug("Sending traefik config for internal interface")
|
||||||
|
self.traefik_route_internal.interface.submit_to_traefik(
|
||||||
|
config=self.traefik_config
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.traefik_route_internal.ready:
|
||||||
|
self._update_service_endpoints()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def databases(self) -> Mapping[str, str]:
|
def databases(self) -> Mapping[str, str]:
|
||||||
@ -453,28 +509,8 @@ class HeatOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def service_name(self) -> str:
|
def service_name(self) -> str:
|
||||||
"""Update service_name to heat-api or heat-api-cfn.
|
"""Service name."""
|
||||||
|
return "heat"
|
||||||
service_name should be updated only once. Get service name from app data if
|
|
||||||
it exists and ignore the charm configuration parameter api-service.
|
|
||||||
If app data does not exist, return with the value from charm configuration.
|
|
||||||
"""
|
|
||||||
service_name = None
|
|
||||||
if hasattr(self, "peers"):
|
|
||||||
service_name = self.leader_get(HEAT_API_SERVICE_KEY)
|
|
||||||
|
|
||||||
if not service_name:
|
|
||||||
service_name = self.config.get("api_service")
|
|
||||||
if service_name not in [
|
|
||||||
HEAT_API_SERVICE_NAME,
|
|
||||||
HEAT_CFN_SERVICE_NAME,
|
|
||||||
]:
|
|
||||||
logger.warning(
|
|
||||||
"Config parameter api_service should be one of heat-api, heat-api-cfn, defaulting to heat-api."
|
|
||||||
)
|
|
||||||
service_name = HEAT_API_SERVICE_NAME
|
|
||||||
|
|
||||||
return service_name
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def service_conf(self) -> str:
|
def service_conf(self) -> str:
|
||||||
@ -494,44 +530,118 @@ class HeatOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
|||||||
@property
|
@property
|
||||||
def service_endpoints(self):
|
def service_endpoints(self):
|
||||||
"""Return heat service endpoints."""
|
"""Return heat service endpoints."""
|
||||||
if self.service_name == HEAT_CFN_SERVICE_NAME:
|
return [
|
||||||
return [
|
{
|
||||||
{
|
"service_name": HEAT_API_CFN_INGRESS_NAME,
|
||||||
"service_name": "heat-cfn",
|
"type": "cloudformation",
|
||||||
"type": "cloudformation",
|
"description": "OpenStack Heat CloudFormation API",
|
||||||
"description": "OpenStack Heat CloudFormation API",
|
"internal_url": f"{self.heat_cfn_internal_url}/v1/$(tenant_id)s",
|
||||||
"internal_url": f"{self.internal_url}/v1/$(tenant_id)s",
|
"public_url": f"{self.heat_cfn_public_url}/v1/$(tenant_id)s",
|
||||||
"public_url": f"{self.public_url}/v1/$(tenant_id)s",
|
"admin_url": f"{self.heat_cfn_admin_url}/v1/$(tenant_id)s",
|
||||||
"admin_url": f"{self.admin_url}/v1/$(tenant_id)s",
|
},
|
||||||
}
|
{
|
||||||
]
|
"service_name": HEAT_API_INGRESS_NAME,
|
||||||
|
"type": "orchestration",
|
||||||
|
"description": "OpenStack Heat API",
|
||||||
|
"internal_url": f"{self.heat_internal_url}/v1/$(tenant_id)s",
|
||||||
|
"public_url": f"{self.heat_public_url}/v1/$(tenant_id)s",
|
||||||
|
"admin_url": f"{self.heat_admin_url}/v1/$(tenant_id)s",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def heat_public_url(self) -> str:
|
||||||
|
"""Url for accessing the public endpoint for heat service."""
|
||||||
|
if self.traefik_route_public and self.traefik_route_public.ready:
|
||||||
|
scheme = self.traefik_route_public.interface.scheme
|
||||||
|
external_host = self.traefik_route_public.interface.external_host
|
||||||
|
public_url = (
|
||||||
|
f"{scheme}://{external_host}/{self.model.name}"
|
||||||
|
f"-{HEAT_API_INGRESS_NAME}"
|
||||||
|
)
|
||||||
|
return self.add_explicit_port(public_url)
|
||||||
else:
|
else:
|
||||||
return [
|
return self.add_explicit_port(
|
||||||
{
|
self.service_url(self.public_ingress_address)
|
||||||
"service_name": "heat",
|
)
|
||||||
"type": "orchestration",
|
|
||||||
"description": "OpenStack Heat API",
|
@property
|
||||||
"internal_url": f"{self.internal_url}/v1/$(tenant_id)s",
|
def heat_cfn_public_url(self) -> str:
|
||||||
"public_url": f"{self.public_url}/v1/$(tenant_id)s",
|
"""Url for accessing the public endpoint for heat cfn service."""
|
||||||
"admin_url": f"{self.admin_url}/v1/$(tenant_id)s",
|
if (
|
||||||
},
|
self.traefik_route_public
|
||||||
]
|
and self.traefik_route_public.interface.is_ready()
|
||||||
|
):
|
||||||
|
scheme = self.traefik_route_public.interface.scheme
|
||||||
|
external_host = self.traefik_route_public.interface.external_host
|
||||||
|
public_url = (
|
||||||
|
f"{scheme}://{external_host}/{self.model.name}"
|
||||||
|
f"-{HEAT_API_CFN_INGRESS_NAME}"
|
||||||
|
)
|
||||||
|
return self.add_explicit_port(public_url)
|
||||||
|
else:
|
||||||
|
return self.add_explicit_port(
|
||||||
|
self.service_url(self.public_ingress_address)
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def heat_internal_url(self) -> str:
|
||||||
|
"""Url for accessing the internal endpoint for heat service."""
|
||||||
|
if self.traefik_route_internal and self.traefik_route_internal.ready:
|
||||||
|
scheme = self.traefik_route_internal.interface.scheme
|
||||||
|
external_host = self.traefik_route_internal.interface.external_host
|
||||||
|
internal_url = (
|
||||||
|
f"{scheme}://{external_host}/{self.model.name}"
|
||||||
|
f"-{HEAT_API_INGRESS_NAME}"
|
||||||
|
)
|
||||||
|
return self.add_explicit_port(internal_url)
|
||||||
|
else:
|
||||||
|
return self.heat_admin_url
|
||||||
|
|
||||||
|
@property
|
||||||
|
def heat_cfn_internal_url(self) -> str:
|
||||||
|
"""Url for accessing the internal endpoint for heat cfn service."""
|
||||||
|
if (
|
||||||
|
self.traefik_route_internal
|
||||||
|
and self.traefik_route_internal.interface.is_ready()
|
||||||
|
):
|
||||||
|
scheme = self.traefik_route_internal.interface.scheme
|
||||||
|
external_host = self.traefik_route_internal.interface.external_host
|
||||||
|
internal_url = (
|
||||||
|
f"{scheme}://{external_host}/{self.model.name}"
|
||||||
|
f"-{HEAT_API_CFN_INGRESS_NAME}"
|
||||||
|
)
|
||||||
|
return self.add_explicit_port(internal_url)
|
||||||
|
else:
|
||||||
|
return self.heat_cfn_admin_url
|
||||||
|
|
||||||
|
@property
|
||||||
|
def heat_admin_url(self) -> str:
|
||||||
|
"""Url for accessing the admin endpoint for heat service."""
|
||||||
|
hostname = self.model.get_binding(
|
||||||
|
"identity-service"
|
||||||
|
).network.ingress_address
|
||||||
|
url = f"http://{hostname}:{HEAT_API_PORT}"
|
||||||
|
return self.add_explicit_port(url)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def heat_cfn_admin_url(self) -> str:
|
||||||
|
"""Url for accessing the admin endpoint for heat service."""
|
||||||
|
hostname = self.model.get_binding(
|
||||||
|
"identity-service"
|
||||||
|
).network.ingress_address
|
||||||
|
url = f"http://{hostname}:{HEAT_API_CFN_PORT}"
|
||||||
|
return self.add_explicit_port(url)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def default_public_ingress_port(self):
|
def default_public_ingress_port(self):
|
||||||
"""Port for Heat API service."""
|
"""Port for Heat API service."""
|
||||||
# Port 8000 if api service is heat-api-cfn
|
return HEAT_API_PORT
|
||||||
if self.service_name == HEAT_CFN_SERVICE_NAME:
|
|
||||||
return 8000
|
|
||||||
|
|
||||||
# Default heat-api port
|
|
||||||
return 8004
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def wsgi_container_name(self) -> str:
|
def wsgi_container_name(self) -> str:
|
||||||
"""Name of the WSGI application container."""
|
"""Name of the WSGI application container."""
|
||||||
# Container name for both heat-api and heat-api-cfn service is heat-api
|
return HEAT_API_CONTAINER
|
||||||
return "heat-api"
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def stack_domain_name(self) -> str:
|
def stack_domain_name(self) -> str:
|
||||||
@ -541,10 +651,7 @@ class HeatOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
|||||||
@property
|
@property
|
||||||
def stack_domain_admin_user(self) -> str:
|
def stack_domain_admin_user(self) -> str:
|
||||||
"""User to manage users and projects in stack_domain_name."""
|
"""User to manage users and projects in stack_domain_name."""
|
||||||
if self.service_name == HEAT_CFN_SERVICE_NAME:
|
return "heat_domain_admin"
|
||||||
return "heat_domain_admin_cfn"
|
|
||||||
else:
|
|
||||||
return "heat_domain_admin"
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def stack_user_role(self) -> str:
|
def stack_user_role(self) -> str:
|
||||||
@ -558,14 +665,37 @@ class HeatOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
|||||||
_cadapters.extend([HeatConfigurationContext(self, "heat")])
|
_cadapters.extend([HeatConfigurationContext(self, "heat")])
|
||||||
return _cadapters
|
return _cadapters
|
||||||
|
|
||||||
def default_container_configs(self):
|
def heat_api_container_configs(self):
|
||||||
"""Return base container configs."""
|
"""Return base container configs."""
|
||||||
return [
|
return [
|
||||||
sunbeam_core.ContainerConfigFile(
|
sunbeam_core.ContainerConfigFile(
|
||||||
"/etc/heat/heat.conf", "root", "heat"
|
"/etc/heat/heat.conf",
|
||||||
|
self.service_user,
|
||||||
|
self.service_group,
|
||||||
|
0o640,
|
||||||
),
|
),
|
||||||
sunbeam_core.ContainerConfigFile(
|
sunbeam_core.ContainerConfigFile(
|
||||||
"/etc/heat/api-paste.ini", "root", "heat"
|
"/etc/heat/api-paste.ini",
|
||||||
|
self.service_user,
|
||||||
|
self.service_group,
|
||||||
|
0o640,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
def heat_api_cfn_container_configs(self):
|
||||||
|
"""Return base container configs."""
|
||||||
|
return [
|
||||||
|
sunbeam_core.ContainerConfigFile(
|
||||||
|
"/etc/heat/heat-api-cfn.conf",
|
||||||
|
self.service_user,
|
||||||
|
self.service_group,
|
||||||
|
0o640,
|
||||||
|
),
|
||||||
|
sunbeam_core.ContainerConfigFile(
|
||||||
|
"/etc/heat/api-paste-cfn.ini",
|
||||||
|
self.service_user,
|
||||||
|
self.service_group,
|
||||||
|
0o640,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -592,80 +722,6 @@ class HeatOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
|||||||
else:
|
else:
|
||||||
logger.warning("Heat stack user role creation failed.")
|
logger.warning("Heat stack user role creation failed.")
|
||||||
|
|
||||||
def _set_config(self, key: str, relation: Relation) -> None:
|
|
||||||
"""Set config key over the relation."""
|
|
||||||
logger.debug(
|
|
||||||
f"Setting config on relation {relation.app.name} {relation.name}/{relation.id}"
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
secret = self.model.get_secret(id=key)
|
|
||||||
logger.debug(
|
|
||||||
f"Granting access to secret {key} for relation "
|
|
||||||
f"{relation.app.name} {relation.name}/{relation.id}"
|
|
||||||
)
|
|
||||||
secret.grant(relation)
|
|
||||||
self.config_svc.interface.set_config(
|
|
||||||
relation=relation,
|
|
||||||
auth_encryption_key=key,
|
|
||||||
)
|
|
||||||
except (ModelError, SecretNotFoundError) as e:
|
|
||||||
logger.debug(
|
|
||||||
f"Error during granting access to secret {key} for "
|
|
||||||
f"relation {relation.app.name} {relation.name}/{relation.id}: "
|
|
||||||
f"{str(e)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def set_config_from_event(self, event: RelationEvent) -> None:
|
|
||||||
"""Set config in relation data."""
|
|
||||||
if not self.unit.is_leader():
|
|
||||||
logger.debug("Not a leader unit, skipping set config")
|
|
||||||
return
|
|
||||||
|
|
||||||
key = self.get_heat_auth_encryption_key_secret()
|
|
||||||
if not key:
|
|
||||||
logger.debug("Auth encryption key not yet set, not sending config")
|
|
||||||
return
|
|
||||||
|
|
||||||
self._set_config(key, event.relation)
|
|
||||||
|
|
||||||
def set_config_on_update(self) -> None:
|
|
||||||
"""Set config on relation on update of local data."""
|
|
||||||
logger.debug(
|
|
||||||
"Update config on all connected heat-shared-config relations"
|
|
||||||
)
|
|
||||||
key = self.get_heat_auth_encryption_key_secret()
|
|
||||||
if not key:
|
|
||||||
logger.info("Auth encryption key not yet set, not sending config")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Send config on all joined heat-service relations
|
|
||||||
for relation in self.framework.model.relations["heat-service"]:
|
|
||||||
self._set_config(key, relation)
|
|
||||||
|
|
||||||
def handle_heat_config_events(self, event: RelationEvent) -> None:
|
|
||||||
"""Handle heat config events.
|
|
||||||
|
|
||||||
This function is called only for heat-k8s instances with api_service
|
|
||||||
heat-api-cfn. Receives auth_encryption_key update from heat-api
|
|
||||||
service via interface heat-service.
|
|
||||||
Update the peer appdata and configure charm for the leader unit.
|
|
||||||
For non-leader units, peer changed event should get triggered which
|
|
||||||
calls configure_charm.
|
|
||||||
"""
|
|
||||||
logger.debug(f"Received event {event}")
|
|
||||||
if isinstance(event, HeatSharedConfigChangedEvent):
|
|
||||||
key = self.heat_config_receiver.interface.auth_encryption_key
|
|
||||||
# Update appdata with auth-encryption-key from heat-api
|
|
||||||
if self.unit.is_leader():
|
|
||||||
logger.debug(
|
|
||||||
"Update Auth encryption key in appdata received from "
|
|
||||||
"heat-service relation event"
|
|
||||||
)
|
|
||||||
self.leader_set({self.heat_auth_encryption_key: key})
|
|
||||||
self.configure_charm(event)
|
|
||||||
else:
|
|
||||||
logger.debug("Not a leader unit, nothing to do")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main(HeatOperatorCharm)
|
main(HeatOperatorCharm)
|
||||||
|
147
charms/heat-k8s/src/templates/api-paste-cfn.ini.j2
Normal file
147
charms/heat-k8s/src/templates/api-paste-cfn.ini.j2
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
|
||||||
|
# heat-api composite
|
||||||
|
[composite:heat-api]
|
||||||
|
paste.composite_factory = heat.api:root_app_factory
|
||||||
|
/: api
|
||||||
|
/healthcheck: healthcheck
|
||||||
|
{% if heat.cfn_ingress_path -%}
|
||||||
|
{{ heat.cfn_ingress_path }}: api
|
||||||
|
{% endif -%}
|
||||||
|
|
||||||
|
# heat-api composite for standalone heat
|
||||||
|
# ie. uses alternative auth backend that authenticates users against keystone
|
||||||
|
# using username and password instead of validating token (which requires
|
||||||
|
# an admin/service token).
|
||||||
|
# To enable, in heat.conf:
|
||||||
|
# [paste_deploy]
|
||||||
|
# flavor = standalone
|
||||||
|
#
|
||||||
|
[composite:heat-api-standalone]
|
||||||
|
paste.composite_factory = heat.api:root_app_factory
|
||||||
|
/: api
|
||||||
|
/healthcheck: healthcheck
|
||||||
|
{% if heat.cfn_ingress_path -%}
|
||||||
|
{{ heat.cfn_ingress_path }}: api
|
||||||
|
{% endif -%}
|
||||||
|
|
||||||
|
# heat-api composite for custom cloud backends
|
||||||
|
# i.e. in heat.conf:
|
||||||
|
# [paste_deploy]
|
||||||
|
# flavor = custombackend
|
||||||
|
#
|
||||||
|
[composite:heat-api-custombackend]
|
||||||
|
paste.composite_factory = heat.api:root_app_factory
|
||||||
|
/: api
|
||||||
|
/healthcheck: healthcheck
|
||||||
|
{% if heat.cfn_ingress_path -%}
|
||||||
|
{{ heat.cfn_ingress_path }}: api
|
||||||
|
{% endif -%}
|
||||||
|
|
||||||
|
# To enable, in heat.conf:
|
||||||
|
# [paste_deploy]
|
||||||
|
# flavor = noauth
|
||||||
|
#
|
||||||
|
[composite:heat-api-noauth]
|
||||||
|
paste.composite_factory = heat.api:root_app_factory
|
||||||
|
/: api
|
||||||
|
/healthcheck: healthcheck
|
||||||
|
{% if heat.cfn_ingress_path -%}
|
||||||
|
{{ heat.cfn_ingress_path }}: api
|
||||||
|
{% endif -%}
|
||||||
|
|
||||||
|
# heat-api-cfn composite
|
||||||
|
[composite:heat-api-cfn]
|
||||||
|
paste.composite_factory = heat.api:root_app_factory
|
||||||
|
/: api-cfn
|
||||||
|
/healthcheck: healthcheck
|
||||||
|
{% if heat.cfn_ingress_path -%}
|
||||||
|
{{ heat.cfn_ingress_path }}: api-cfn
|
||||||
|
{% endif -%}
|
||||||
|
|
||||||
|
# heat-api-cfn composite for standalone heat
|
||||||
|
# relies exclusively on authenticating with ec2 signed requests
|
||||||
|
[composite:heat-api-cfn-standalone]
|
||||||
|
paste.composite_factory = heat.api:root_app_factory
|
||||||
|
/: api-cfn
|
||||||
|
/healthcheck: healthcheck
|
||||||
|
{% if heat.cfn_ingress_path -%}
|
||||||
|
{{ heat.cfn_ingress_path }}: api-cfn
|
||||||
|
{% endif -%}
|
||||||
|
|
||||||
|
[composite:api]
|
||||||
|
paste.composite_factory = heat.api:pipeline_factory
|
||||||
|
default = cors request_id faultwrap http_proxy_to_wsgi versionnegotiation authurl authtoken context osprofiler apiv1app
|
||||||
|
standalone = cors request_id faultwrap http_proxy_to_wsgi versionnegotiation authurl authpassword context apiv1app
|
||||||
|
custombackend = cors request_id context faultwrap versionnegotiation custombackendauth apiv1app
|
||||||
|
noauth = cors request_id faultwrap noauth context http_proxy_to_wsgi versionnegotiation apiv1app
|
||||||
|
|
||||||
|
[composite:api-cfn]
|
||||||
|
paste.composite_factory = heat.api:pipeline_factory
|
||||||
|
default = cors request_id http_proxy_to_wsgi cfnversionnegotiation ec2authtoken authtoken context osprofiler apicfnv1app
|
||||||
|
standalone = cors request_id http_proxy_to_wsgi cfnversionnegotiation ec2authtoken context apicfnv1app
|
||||||
|
|
||||||
|
[app:apiv1app]
|
||||||
|
paste.app_factory = heat.common.wsgi:app_factory
|
||||||
|
heat.app_factory = heat.api.openstack.v1:API
|
||||||
|
|
||||||
|
[app:apicfnv1app]
|
||||||
|
paste.app_factory = heat.common.wsgi:app_factory
|
||||||
|
heat.app_factory = heat.api.cfn.v1:API
|
||||||
|
|
||||||
|
[app:healthcheck]
|
||||||
|
paste.app_factory = oslo_middleware:Healthcheck.app_factory
|
||||||
|
|
||||||
|
[filter:versionnegotiation]
|
||||||
|
paste.filter_factory = heat.common.wsgi:filter_factory
|
||||||
|
heat.filter_factory = heat.api.openstack:version_negotiation_filter
|
||||||
|
|
||||||
|
[filter:cors]
|
||||||
|
paste.filter_factory = oslo_middleware.cors:filter_factory
|
||||||
|
oslo_config_project = heat
|
||||||
|
|
||||||
|
[filter:faultwrap]
|
||||||
|
paste.filter_factory = heat.common.wsgi:filter_factory
|
||||||
|
heat.filter_factory = heat.api.openstack:faultwrap_filter
|
||||||
|
|
||||||
|
[filter:cfnversionnegotiation]
|
||||||
|
paste.filter_factory = heat.common.wsgi:filter_factory
|
||||||
|
heat.filter_factory = heat.api.cfn:version_negotiation_filter
|
||||||
|
|
||||||
|
[filter:cwversionnegotiation]
|
||||||
|
paste.filter_factory = heat.common.wsgi:filter_factory
|
||||||
|
|
||||||
|
[filter:context]
|
||||||
|
paste.filter_factory = heat.common.context:ContextMiddleware_filter_factory
|
||||||
|
|
||||||
|
[filter:ec2authtoken]
|
||||||
|
paste.filter_factory = heat.api.aws.ec2token:EC2Token_filter_factory
|
||||||
|
|
||||||
|
[filter:http_proxy_to_wsgi]
|
||||||
|
paste.filter_factory = oslo_middleware:HTTPProxyToWSGI.factory
|
||||||
|
|
||||||
|
# Middleware to set auth_url header appropriately
|
||||||
|
[filter:authurl]
|
||||||
|
paste.filter_factory = heat.common.auth_url:filter_factory
|
||||||
|
|
||||||
|
# Auth middleware that validates token against keystone
|
||||||
|
[filter:authtoken]
|
||||||
|
paste.filter_factory = keystonemiddleware.auth_token:filter_factory
|
||||||
|
|
||||||
|
# Auth middleware that validates username/password against keystone
|
||||||
|
[filter:authpassword]
|
||||||
|
paste.filter_factory = heat.common.auth_password:filter_factory
|
||||||
|
|
||||||
|
# Auth middleware that validates against custom backend
|
||||||
|
[filter:custombackendauth]
|
||||||
|
paste.filter_factory = heat.common.custom_backend_auth:filter_factory
|
||||||
|
|
||||||
|
# Auth middleware that accepts any auth
|
||||||
|
[filter:noauth]
|
||||||
|
paste.filter_factory = heat.common.noauth:filter_factory
|
||||||
|
|
||||||
|
# Middleware to set x-openstack-request-id in http response header
|
||||||
|
[filter:request_id]
|
||||||
|
paste.filter_factory = oslo_middleware.request_id:RequestId.factory
|
||||||
|
|
||||||
|
[filter:osprofiler]
|
||||||
|
paste.filter_factory = osprofiler.web:WsgiMiddleware.factory
|
@ -4,8 +4,8 @@
|
|||||||
paste.composite_factory = heat.api:root_app_factory
|
paste.composite_factory = heat.api:root_app_factory
|
||||||
/: api
|
/: api
|
||||||
/healthcheck: healthcheck
|
/healthcheck: healthcheck
|
||||||
{% if ingress_public.ingress_path -%}
|
{% if heat.ingress_path -%}
|
||||||
{{ ingress_public.ingress_path }}: api
|
{{ heat.ingress_path }}: api
|
||||||
{% endif -%}
|
{% endif -%}
|
||||||
|
|
||||||
# heat-api composite for standalone heat
|
# heat-api composite for standalone heat
|
||||||
@ -20,8 +20,8 @@ paste.composite_factory = heat.api:root_app_factory
|
|||||||
paste.composite_factory = heat.api:root_app_factory
|
paste.composite_factory = heat.api:root_app_factory
|
||||||
/: api
|
/: api
|
||||||
/healthcheck: healthcheck
|
/healthcheck: healthcheck
|
||||||
{% if ingress_public.ingress_path -%}
|
{% if heat.ingress_path -%}
|
||||||
{{ ingress_public.ingress_path }}: api
|
{{ heat.ingress_path }}: api
|
||||||
{% endif -%}
|
{% endif -%}
|
||||||
|
|
||||||
# heat-api composite for custom cloud backends
|
# heat-api composite for custom cloud backends
|
||||||
@ -33,8 +33,8 @@ paste.composite_factory = heat.api:root_app_factory
|
|||||||
paste.composite_factory = heat.api:root_app_factory
|
paste.composite_factory = heat.api:root_app_factory
|
||||||
/: api
|
/: api
|
||||||
/healthcheck: healthcheck
|
/healthcheck: healthcheck
|
||||||
{% if ingress_public.ingress_path -%}
|
{% if heat.ingress_path -%}
|
||||||
{{ ingress_public.ingress_path }}: api
|
{{ heat.ingress_path }}: api
|
||||||
{% endif -%}
|
{% endif -%}
|
||||||
|
|
||||||
# To enable, in heat.conf:
|
# To enable, in heat.conf:
|
||||||
@ -45,8 +45,8 @@ paste.composite_factory = heat.api:root_app_factory
|
|||||||
paste.composite_factory = heat.api:root_app_factory
|
paste.composite_factory = heat.api:root_app_factory
|
||||||
/: api
|
/: api
|
||||||
/healthcheck: healthcheck
|
/healthcheck: healthcheck
|
||||||
{% if ingress_public.ingress_path -%}
|
{% if heat.ingress_path -%}
|
||||||
{{ ingress_public.ingress_path }}: api
|
{{ heat.ingress_path }}: api
|
||||||
{% endif -%}
|
{% endif -%}
|
||||||
|
|
||||||
# heat-api-cfn composite
|
# heat-api-cfn composite
|
||||||
@ -54,8 +54,8 @@ paste.composite_factory = heat.api:root_app_factory
|
|||||||
paste.composite_factory = heat.api:root_app_factory
|
paste.composite_factory = heat.api:root_app_factory
|
||||||
/: api-cfn
|
/: api-cfn
|
||||||
/healthcheck: healthcheck
|
/healthcheck: healthcheck
|
||||||
{% if ingress_public.ingress_path -%}
|
{% if heat.ingress_path -%}
|
||||||
{{ ingress_public.ingress_path }}: api-cfn
|
{{ heat.ingress_path }}: api-cfn
|
||||||
{% endif -%}
|
{% endif -%}
|
||||||
|
|
||||||
# heat-api-cfn composite for standalone heat
|
# heat-api-cfn composite for standalone heat
|
||||||
@ -64,8 +64,8 @@ paste.composite_factory = heat.api:root_app_factory
|
|||||||
paste.composite_factory = heat.api:root_app_factory
|
paste.composite_factory = heat.api:root_app_factory
|
||||||
/: api-cfn
|
/: api-cfn
|
||||||
/healthcheck: healthcheck
|
/healthcheck: healthcheck
|
||||||
{% if ingress_public.ingress_path -%}
|
{% if heat.ingress_path -%}
|
||||||
{{ ingress_public.ingress_path }}: api-cfn
|
{{ heat.ingress_path }}: api-cfn
|
||||||
{% endif -%}
|
{% endif -%}
|
||||||
|
|
||||||
[composite:api]
|
[composite:api]
|
||||||
|
34
charms/heat-k8s/src/templates/heat-api-cfn.conf.j2
Normal file
34
charms/heat-k8s/src/templates/heat-api-cfn.conf.j2
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
[DEFAULT]
|
||||||
|
debug = {{ options.debug }}
|
||||||
|
|
||||||
|
instance_driver=heat.engine.nova
|
||||||
|
plugin_dirs = /usr/lib64/heat,/usr/lib/heat
|
||||||
|
environment_dir=/etc/heat/environment.d
|
||||||
|
host=heat
|
||||||
|
auth_encryption_key={{ heat.auth_encryption_key }}
|
||||||
|
stack_domain_admin={{ heat.stack_domain_admin_user }}
|
||||||
|
stack_domain_admin_password={{ heat.stack_domain_admin_password }}
|
||||||
|
stack_user_domain_name={{ heat.stack_domain_name }}
|
||||||
|
|
||||||
|
transport_url = {{ amqp.transport_url }}
|
||||||
|
|
||||||
|
num_engine_workers = 4
|
||||||
|
|
||||||
|
{% include "parts/section-database" %}
|
||||||
|
|
||||||
|
{% include "parts/section-identity" %}
|
||||||
|
|
||||||
|
|
||||||
|
[paste_deploy]
|
||||||
|
api_paste_config=/etc/heat/api-paste-cfn.ini
|
||||||
|
|
||||||
|
[heat_api]
|
||||||
|
bind_port = 8004
|
||||||
|
workers = 4
|
||||||
|
|
||||||
|
[heat_api_cfn]
|
||||||
|
bind_port = 8000
|
||||||
|
workers = 4
|
||||||
|
|
||||||
|
{% include "parts/section-oslo-messaging-rabbit" %}
|
||||||
|
|
@ -1,27 +0,0 @@
|
|||||||
Listen {{ wsgi_config.public_port }}
|
|
||||||
<VirtualHost *:{{ wsgi_config.public_port }}>
|
|
||||||
WSGIDaemonProcess {{ wsgi_config.group }} processes=3 threads=1 user={{ wsgi_config.user }} group={{ wsgi_config.group }} \
|
|
||||||
display-name=%{GROUP}
|
|
||||||
WSGIProcessGroup {{ wsgi_config.group }}
|
|
||||||
{% if ingress_internal.ingress_path -%}
|
|
||||||
WSGIScriptAlias {{ ingress_internal.ingress_path }} {{ wsgi_config.wsgi_public_script }}
|
|
||||||
{% endif -%}
|
|
||||||
WSGIScriptAlias / {{ wsgi_config.wsgi_public_script }}
|
|
||||||
WSGIApplicationGroup %{GLOBAL}
|
|
||||||
WSGIPassAuthorization On
|
|
||||||
<IfVersion >= 2.4>
|
|
||||||
ErrorLogFormat "%{cu}t %M"
|
|
||||||
</IfVersion>
|
|
||||||
ErrorLog {{ wsgi_config.error_log }}
|
|
||||||
CustomLog {{ wsgi_config.custom_log }} combined
|
|
||||||
|
|
||||||
<Directory /usr/bin>
|
|
||||||
<IfVersion >= 2.4>
|
|
||||||
Require all granted
|
|
||||||
</IfVersion>
|
|
||||||
<IfVersion < 2.4>
|
|
||||||
Order allow,deny
|
|
||||||
Allow from all
|
|
||||||
</IfVersion>
|
|
||||||
</Directory>
|
|
||||||
</VirtualHost>
|
|
@ -1,27 +0,0 @@
|
|||||||
Listen {{ wsgi_config.public_port }}
|
|
||||||
<VirtualHost *:{{ wsgi_config.public_port }}>
|
|
||||||
WSGIDaemonProcess {{ wsgi_config.group }} processes=3 threads=1 user={{ wsgi_config.user }} group={{ wsgi_config.group }} \
|
|
||||||
display-name=%{GROUP}
|
|
||||||
WSGIProcessGroup {{ wsgi_config.group }}
|
|
||||||
{% if ingress_internal.ingress_path -%}
|
|
||||||
WSGIScriptAlias {{ ingress_internal.ingress_path }} {{ wsgi_config.wsgi_public_script }}
|
|
||||||
{% endif -%}
|
|
||||||
WSGIScriptAlias / {{ wsgi_config.wsgi_public_script }}
|
|
||||||
WSGIApplicationGroup %{GLOBAL}
|
|
||||||
WSGIPassAuthorization On
|
|
||||||
<IfVersion >= 2.4>
|
|
||||||
ErrorLogFormat "%{cu}t %M"
|
|
||||||
</IfVersion>
|
|
||||||
ErrorLog {{ wsgi_config.error_log }}
|
|
||||||
CustomLog {{ wsgi_config.custom_log }} combined
|
|
||||||
|
|
||||||
<Directory /usr/bin>
|
|
||||||
<IfVersion >= 2.4>
|
|
||||||
Require all granted
|
|
||||||
</IfVersion>
|
|
||||||
<IfVersion < 2.4>
|
|
||||||
Order allow,deny
|
|
||||||
Allow from all
|
|
||||||
</IfVersion>
|
|
||||||
</Directory>
|
|
||||||
</VirtualHost>
|
|
@ -50,16 +50,6 @@ applications:
|
|||||||
heat-api-image: ghcr.io/canonical/heat-consolidated:2023.2
|
heat-api-image: ghcr.io/canonical/heat-consolidated:2023.2
|
||||||
heat-engine-image: ghcr.io/canonical/heat-consolidated:2023.2
|
heat-engine-image: ghcr.io/canonical/heat-consolidated:2023.2
|
||||||
|
|
||||||
heat-cfn:
|
|
||||||
charm: ../../heat-k8s.charm
|
|
||||||
scale: 1
|
|
||||||
trust: true
|
|
||||||
options:
|
|
||||||
api_service: heat-api-cfn
|
|
||||||
resources:
|
|
||||||
heat-api-image: ghcr.io/canonical/heat-consolidated:2023.2
|
|
||||||
heat-engine-image: ghcr.io/canonical/heat-consolidated:2023.2
|
|
||||||
|
|
||||||
relations:
|
relations:
|
||||||
- - traefik:ingress
|
- - traefik:ingress
|
||||||
- keystone:ingress-internal
|
- keystone:ingress-internal
|
||||||
@ -75,24 +65,9 @@ relations:
|
|||||||
- heat:identity-service
|
- heat:identity-service
|
||||||
- - keystone:identity-ops
|
- - keystone:identity-ops
|
||||||
- heat:identity-ops
|
- heat:identity-ops
|
||||||
- - traefik:ingress
|
- - traefik:traefik-route
|
||||||
- heat:ingress-internal
|
- heat:traefik-route-internal
|
||||||
- - traefik-public:ingress
|
- - traefik-public:traefik-route
|
||||||
- heat:ingress-public
|
- heat:traefik-route-public
|
||||||
- - rabbitmq:amqp
|
- - rabbitmq:amqp
|
||||||
- heat:amqp
|
- heat:amqp
|
||||||
|
|
||||||
- - mysql:database
|
|
||||||
- heat-cfn:database
|
|
||||||
- - keystone:identity-service
|
|
||||||
- heat-cfn:identity-service
|
|
||||||
- - keystone:identity-ops
|
|
||||||
- heat-cfn:identity-ops
|
|
||||||
- - traefik:ingress
|
|
||||||
- heat-cfn:ingress-internal
|
|
||||||
- - traefik-public:ingress
|
|
||||||
- heat-cfn:ingress-public
|
|
||||||
- - rabbitmq:amqp
|
|
||||||
- heat-cfn:amqp
|
|
||||||
- - heat:heat-service
|
|
||||||
- heat-cfn:heat-config
|
|
||||||
|
@ -36,6 +36,3 @@ target_deploy_status:
|
|||||||
heat:
|
heat:
|
||||||
workload-status: active
|
workload-status: active
|
||||||
workload-status-message-regex: '^.*$'
|
workload-status-message-regex: '^.*$'
|
||||||
heat-cfn:
|
|
||||||
workload-status: active
|
|
||||||
workload-status-message-regex: '^.*$'
|
|
||||||
|
@ -104,14 +104,17 @@ class TestHeatOperatorCharm(test_utils.CharmTestCase):
|
|||||||
)
|
)
|
||||||
return rel_id
|
return rel_id
|
||||||
|
|
||||||
def add_complete_heat_shared_config_relation(
|
def add_complete_ingress_relation(self, harness: Harness) -> None:
|
||||||
self, harness: Harness
|
"""Add complete traefik-route relations."""
|
||||||
) -> None:
|
|
||||||
"""Add complete Heat shared config relation."""
|
|
||||||
harness.add_relation(
|
harness.add_relation(
|
||||||
"heat-config",
|
"traefik-route-public",
|
||||||
"heat",
|
"heat",
|
||||||
app_data={"auth-encryption-key": "fake-secret"},
|
app_data={"external_host": "dummy-ip", "scheme": "http"},
|
||||||
|
)
|
||||||
|
harness.add_relation(
|
||||||
|
"traefik-route-internal",
|
||||||
|
"heat",
|
||||||
|
app_data={"external_host": "dummy-ip", "scheme": "http"},
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_pebble_ready_handler(self):
|
def test_pebble_ready_handler(self):
|
||||||
@ -129,7 +132,7 @@ class TestHeatOperatorCharm(test_utils.CharmTestCase):
|
|||||||
|
|
||||||
self.assertEqual(self.harness.charm.seen_events, [])
|
self.assertEqual(self.harness.charm.seen_events, [])
|
||||||
test_utils.set_all_pebbles_ready(self.harness)
|
test_utils.set_all_pebbles_ready(self.harness)
|
||||||
self.assertEqual(len(self.harness.charm.seen_events), 2)
|
self.assertEqual(len(self.harness.charm.seen_events), 3)
|
||||||
|
|
||||||
def test_all_relations(self):
|
def test_all_relations(self):
|
||||||
"""Test all integrations for operator."""
|
"""Test all integrations for operator."""
|
||||||
@ -148,9 +151,8 @@ class TestHeatOperatorCharm(test_utils.CharmTestCase):
|
|||||||
test_utils.set_all_pebbles_ready(self.harness)
|
test_utils.set_all_pebbles_ready(self.harness)
|
||||||
# this adds all the default/common relations
|
# this adds all the default/common relations
|
||||||
test_utils.add_all_relations(self.harness)
|
test_utils.add_all_relations(self.harness)
|
||||||
test_utils.add_complete_ingress_relation(self.harness)
|
self.add_complete_ingress_relation(self.harness)
|
||||||
self.add_complete_identity_resource_relation(self.harness)
|
self.add_complete_identity_resource_relation(self.harness)
|
||||||
self.add_complete_heat_shared_config_relation(self.harness)
|
|
||||||
|
|
||||||
setup_cmds = [["heat-manage", "db_sync"]]
|
setup_cmds = [["heat-manage", "db_sync"]]
|
||||||
for cmd in setup_cmds:
|
for cmd in setup_cmds:
|
||||||
@ -158,3 +160,9 @@ class TestHeatOperatorCharm(test_utils.CharmTestCase):
|
|||||||
config_files = ["/etc/heat/heat.conf", "/etc/heat/api-paste.ini"]
|
config_files = ["/etc/heat/heat.conf", "/etc/heat/api-paste.ini"]
|
||||||
for f in config_files:
|
for f in config_files:
|
||||||
self.check_file("heat-api", f)
|
self.check_file("heat-api", f)
|
||||||
|
config_files = [
|
||||||
|
"/etc/heat/heat-api-cfn.conf",
|
||||||
|
"/etc/heat/api-paste-cfn.ini",
|
||||||
|
]
|
||||||
|
for f in config_files:
|
||||||
|
self.check_file("heat-api-cfn", f)
|
||||||
|
Loading…
Reference in New Issue
Block a user