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.
|
||||
type: boolean
|
||||
os-admin-hostname:
|
||||
default: glance.juju
|
||||
default: heat.juju
|
||||
description: |
|
||||
The hostname or address of the admin endpoints that should be advertised
|
||||
in the glance image provider.
|
||||
type: string
|
||||
os-internal-hostname:
|
||||
default: glance.juju
|
||||
default: heat.juju
|
||||
description: |
|
||||
The hostname or address of the internal endpoints that should be advertised
|
||||
in the glance image provider.
|
||||
type: string
|
||||
os-public-hostname:
|
||||
default: glance.juju
|
||||
default: heat.juju
|
||||
description: |
|
||||
The hostname or address of the internal endpoints that should be advertised
|
||||
in the glance image provider.
|
||||
@ -25,10 +25,3 @@ options:
|
||||
default: RegionOne
|
||||
description: Space delimited list of OpenStack regions
|
||||
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.v1.identity_service
|
||||
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:
|
||||
heat-api:
|
||||
resource: heat-api-image
|
||||
heat-api-cfn:
|
||||
resource: heat-api-image
|
||||
heat-engine:
|
||||
resource: heat-engine-image
|
||||
|
||||
@ -41,24 +43,17 @@ requires:
|
||||
limit: 1
|
||||
identity-service:
|
||||
interface: keystone
|
||||
ingress-internal:
|
||||
interface: ingress
|
||||
traefik-route-internal:
|
||||
interface: traefik_route
|
||||
optional: true
|
||||
limit: 1
|
||||
ingress-public:
|
||||
interface: ingress
|
||||
traefik-route-public:
|
||||
interface: traefik_route
|
||||
limit: 1
|
||||
amqp:
|
||||
interface: rabbitmq
|
||||
identity-ops:
|
||||
interface: keystone-resources
|
||||
heat-config:
|
||||
interface: heat-shared-config
|
||||
optional: true
|
||||
|
||||
provides:
|
||||
heat-service:
|
||||
interface: heat-shared-config
|
||||
|
||||
peers:
|
||||
peers:
|
||||
|
@ -22,6 +22,7 @@ import hashlib
|
||||
import json
|
||||
import logging
|
||||
import secrets
|
||||
import socket
|
||||
from typing import (
|
||||
Callable,
|
||||
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.core as sunbeam_core
|
||||
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 (
|
||||
CharmBase,
|
||||
RelationEvent,
|
||||
)
|
||||
from ops.framework import (
|
||||
@ -53,130 +47,74 @@ from ops.main import (
|
||||
)
|
||||
from ops.model import (
|
||||
ModelError,
|
||||
Relation,
|
||||
SecretNotFoundError,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
CREDENTIALS_SECRET_PREFIX = "credentials_"
|
||||
HEAT_API_CONTAINER = "heat-api"
|
||||
HEAT_API_CFN_CONTAINER = "heat-api-cfn"
|
||||
HEAT_ENGINE_CONTAINER = "heat-engine"
|
||||
HEAT_API_SERVICE_KEY = "api-service"
|
||||
HEAT_API_SERVICE_NAME = "heat-api"
|
||||
HEAT_CFN_SERVICE_NAME = "heat-api-cfn"
|
||||
HEAT_API_INGRESS_NAME = "heat"
|
||||
HEAT_API_CFN_INGRESS_NAME = "heat-cfn"
|
||||
HEAT_API_PORT = 8004
|
||||
HEAT_API_CFN_PORT = 8000
|
||||
|
||||
|
||||
class HeatSharedConfigProvidesHandler(sunbeam_rhandlers.RelationHandler):
|
||||
"""Handler for heat shared config relation on provider side."""
|
||||
class TraefikRouteHandler(sunbeam_rhandlers.RelationHandler):
|
||||
"""Base class to handle traefik route relations."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
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,
|
||||
charm: ops.charm.CharmBase,
|
||||
relation_name: str,
|
||||
callback_f: Callable,
|
||||
mandatory: bool = False,
|
||||
):
|
||||
"""Create a new heat-shared-config handler.
|
||||
|
||||
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
|
||||
"""
|
||||
) -> None:
|
||||
"""Run constructor."""
|
||||
super().__init__(charm, relation_name, callback_f, mandatory)
|
||||
|
||||
def setup_event_handler(self) -> None:
|
||||
"""Configure event handlers for Heat shared config relation."""
|
||||
logger.debug("Setting up Heat shared config event handler")
|
||||
svc = HeatSharedConfigRequires(
|
||||
def setup_event_handler(self) -> ops.charm.Object:
|
||||
"""Configure event handlers for an Ingress relation."""
|
||||
logger.debug("Setting up ingress event handler")
|
||||
from charms.traefik_route_k8s.v0.traefik_route import (
|
||||
TraefikRouteRequirer,
|
||||
)
|
||||
|
||||
interface = TraefikRouteRequirer(
|
||||
self.charm,
|
||||
self.model.get_relation(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:
|
||||
"""Handle config_changed event."""
|
||||
logger.debug(
|
||||
"Heat shared config provider config changed event received"
|
||||
self.framework.observe(interface.on.ready, self._on_ingress_ready)
|
||||
self.framework.observe(
|
||||
self.charm.on[self.relation_name].relation_joined,
|
||||
self._on_traefik_relation_joined,
|
||||
)
|
||||
self.callback_f(event)
|
||||
return interface
|
||||
|
||||
def _on_goneaway(self, event: RelationEvent) -> None:
|
||||
"""Handle gone_away event."""
|
||||
logger.debug("Heat shared config relation is departed/broken")
|
||||
self.callback_f(event)
|
||||
def _on_traefik_relation_joined(self, event: RelationEvent) -> None:
|
||||
"""Handle traefik relation joined event."""
|
||||
# This is passed as None during the init method, so update the
|
||||
# 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
|
||||
def ready(self) -> bool:
|
||||
"""Whether handler is ready for use."""
|
||||
try:
|
||||
return bool(self.interface.auth_encryption_key)
|
||||
except (AttributeError, KeyError):
|
||||
return False
|
||||
"""Whether the handler is ready for use."""
|
||||
if self.charm.unit.is_leader():
|
||||
return bool(self.interface.external_host)
|
||||
else:
|
||||
return self.interface.is_ready()
|
||||
|
||||
|
||||
class HeatAPIPebbleHandler(sunbeam_chandlers.ServicePebbleHandler):
|
||||
@ -188,36 +126,20 @@ class HeatAPIPebbleHandler(sunbeam_chandlers.ServicePebbleHandler):
|
||||
:returns: pebble service layer configuration for heat api service
|
||||
:rtype: dict
|
||||
"""
|
||||
if self.charm.service_name == HEAT_CFN_SERVICE_NAME:
|
||||
return {
|
||||
"summary": "heat api cfn layer",
|
||||
"description": "pebble configuration for heat api cfn service",
|
||||
"services": {
|
||||
"heat-api": {
|
||||
"override": "replace",
|
||||
"summary": "Heat API CFN",
|
||||
"command": "heat-api-cfn",
|
||||
"startup": "enabled",
|
||||
"user": "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",
|
||||
}
|
||||
},
|
||||
}
|
||||
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:
|
||||
"""Health check pebble layer.
|
||||
@ -230,7 +152,49 @@ class HeatAPIPebbleHandler(sunbeam_chandlers.ServicePebbleHandler):
|
||||
"override": "replace",
|
||||
"level": "ready",
|
||||
"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_password": password,
|
||||
"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",
|
||||
"amqp",
|
||||
"identity-service",
|
||||
"ingress-public",
|
||||
"traefik-route-public",
|
||||
"identity-ops",
|
||||
}
|
||||
|
||||
def __init__(self, framework):
|
||||
self.traefik_route_public = None
|
||||
self.traefik_route_internal = None
|
||||
super().__init__(framework)
|
||||
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:
|
||||
"""Return the sha1 of the requested ops."""
|
||||
@ -333,26 +314,20 @@ class HeatOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
||||
)
|
||||
handlers.append(self.user_id_ops)
|
||||
|
||||
if self.service_name == HEAT_CFN_SERVICE_NAME:
|
||||
# heat-config is not a mandatory relation.
|
||||
# If instance of heat-k8s deployed with service heat-api-cfn and
|
||||
# without any heat-api, heat-api-cfn workload should still come
|
||||
# to active using internally generated auth-encryption-key
|
||||
self.heat_config_receiver = HeatSharedConfigRequiresHandler(
|
||||
self,
|
||||
"heat-config",
|
||||
self.handle_heat_config_events,
|
||||
"heat-config" in self.mandatory_relations,
|
||||
)
|
||||
handlers.append(self.heat_config_receiver)
|
||||
else:
|
||||
self.config_svc = HeatSharedConfigProvidesHandler(
|
||||
self,
|
||||
"heat-service",
|
||||
self.set_config_from_event,
|
||||
)
|
||||
handlers.append(self.config_svc)
|
||||
|
||||
self.traefik_route_public = TraefikRouteHandler(
|
||||
self,
|
||||
"traefik-route-public",
|
||||
self.handle_traefik_ready,
|
||||
"traefik-route-public" in self.mandatory_relations,
|
||||
)
|
||||
handlers.append(self.traefik_route_public)
|
||||
self.traefik_route_internal = TraefikRouteHandler(
|
||||
self,
|
||||
"traefik-route-internal",
|
||||
self.handle_traefik_ready,
|
||||
"traefik-route-internal" in self.mandatory_relations,
|
||||
)
|
||||
handlers.append(self.traefik_route_internal)
|
||||
return handlers
|
||||
|
||||
def get_pebble_handlers(
|
||||
@ -364,7 +339,15 @@ class HeatOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
||||
self,
|
||||
HEAT_API_CONTAINER,
|
||||
"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.configure_charm,
|
||||
),
|
||||
@ -372,7 +355,7 @@ class HeatOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
||||
self,
|
||||
HEAT_ENGINE_CONTAINER,
|
||||
"heat-engine",
|
||||
self.default_container_configs(),
|
||||
self.heat_api_container_configs(),
|
||||
self.template_dir,
|
||||
self.configure_charm,
|
||||
),
|
||||
@ -423,23 +406,96 @@ class HeatOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
||||
else:
|
||||
logger.debug("Creating auth key")
|
||||
self.set_heat_auth_encryption_key()
|
||||
if self.service_name == HEAT_API_SERVICE_NAME:
|
||||
# Send Auth encryption key over heat-service relation
|
||||
self.set_config_on_update()
|
||||
|
||||
self.handle_traefik_ready(event)
|
||||
|
||||
super().configure_charm(event)
|
||||
|
||||
def configure_app_leader(self, event):
|
||||
"""Configure app leader.
|
||||
@property
|
||||
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
|
||||
does not exist.
|
||||
"""
|
||||
super().configure_app_leader(event)
|
||||
# Get host key value from all units
|
||||
hosts = self.peers.get_all_unit_values(
|
||||
key="host", include_local_unit=True
|
||||
)
|
||||
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
|
||||
if not self.leader_get(HEAT_API_SERVICE_KEY):
|
||||
self.leader_set({HEAT_API_SERVICE_KEY: self.service_name})
|
||||
config = {
|
||||
"http": {
|
||||
"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
|
||||
def databases(self) -> Mapping[str, str]:
|
||||
@ -453,28 +509,8 @@ class HeatOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
||||
|
||||
@property
|
||||
def service_name(self) -> str:
|
||||
"""Update service_name to heat-api or heat-api-cfn.
|
||||
|
||||
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
|
||||
"""Service name."""
|
||||
return "heat"
|
||||
|
||||
@property
|
||||
def service_conf(self) -> str:
|
||||
@ -494,44 +530,118 @@ class HeatOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
||||
@property
|
||||
def service_endpoints(self):
|
||||
"""Return heat service endpoints."""
|
||||
if self.service_name == HEAT_CFN_SERVICE_NAME:
|
||||
return [
|
||||
{
|
||||
"service_name": "heat-cfn",
|
||||
"type": "cloudformation",
|
||||
"description": "OpenStack Heat CloudFormation API",
|
||||
"internal_url": f"{self.internal_url}/v1/$(tenant_id)s",
|
||||
"public_url": f"{self.public_url}/v1/$(tenant_id)s",
|
||||
"admin_url": f"{self.admin_url}/v1/$(tenant_id)s",
|
||||
}
|
||||
]
|
||||
return [
|
||||
{
|
||||
"service_name": HEAT_API_CFN_INGRESS_NAME,
|
||||
"type": "cloudformation",
|
||||
"description": "OpenStack Heat CloudFormation API",
|
||||
"internal_url": f"{self.heat_cfn_internal_url}/v1/$(tenant_id)s",
|
||||
"public_url": f"{self.heat_cfn_public_url}/v1/$(tenant_id)s",
|
||||
"admin_url": f"{self.heat_cfn_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:
|
||||
return [
|
||||
{
|
||||
"service_name": "heat",
|
||||
"type": "orchestration",
|
||||
"description": "OpenStack Heat API",
|
||||
"internal_url": f"{self.internal_url}/v1/$(tenant_id)s",
|
||||
"public_url": f"{self.public_url}/v1/$(tenant_id)s",
|
||||
"admin_url": f"{self.admin_url}/v1/$(tenant_id)s",
|
||||
},
|
||||
]
|
||||
return self.add_explicit_port(
|
||||
self.service_url(self.public_ingress_address)
|
||||
)
|
||||
|
||||
@property
|
||||
def heat_cfn_public_url(self) -> str:
|
||||
"""Url for accessing the public endpoint for heat cfn service."""
|
||||
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
|
||||
def default_public_ingress_port(self):
|
||||
"""Port for Heat API service."""
|
||||
# Port 8000 if api service is heat-api-cfn
|
||||
if self.service_name == HEAT_CFN_SERVICE_NAME:
|
||||
return 8000
|
||||
|
||||
# Default heat-api port
|
||||
return 8004
|
||||
return HEAT_API_PORT
|
||||
|
||||
@property
|
||||
def wsgi_container_name(self) -> str:
|
||||
"""Name of the WSGI application container."""
|
||||
# Container name for both heat-api and heat-api-cfn service is heat-api
|
||||
return "heat-api"
|
||||
return HEAT_API_CONTAINER
|
||||
|
||||
@property
|
||||
def stack_domain_name(self) -> str:
|
||||
@ -541,10 +651,7 @@ class HeatOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
||||
@property
|
||||
def stack_domain_admin_user(self) -> str:
|
||||
"""User to manage users and projects in stack_domain_name."""
|
||||
if self.service_name == HEAT_CFN_SERVICE_NAME:
|
||||
return "heat_domain_admin_cfn"
|
||||
else:
|
||||
return "heat_domain_admin"
|
||||
return "heat_domain_admin"
|
||||
|
||||
@property
|
||||
def stack_user_role(self) -> str:
|
||||
@ -558,14 +665,37 @@ class HeatOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
||||
_cadapters.extend([HeatConfigurationContext(self, "heat")])
|
||||
return _cadapters
|
||||
|
||||
def default_container_configs(self):
|
||||
def heat_api_container_configs(self):
|
||||
"""Return base container configs."""
|
||||
return [
|
||||
sunbeam_core.ContainerConfigFile(
|
||||
"/etc/heat/heat.conf", "root", "heat"
|
||||
"/etc/heat/heat.conf",
|
||||
self.service_user,
|
||||
self.service_group,
|
||||
0o640,
|
||||
),
|
||||
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:
|
||||
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__":
|
||||
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
|
||||
/: api
|
||||
/healthcheck: healthcheck
|
||||
{% if ingress_public.ingress_path -%}
|
||||
{{ ingress_public.ingress_path }}: api
|
||||
{% if heat.ingress_path -%}
|
||||
{{ heat.ingress_path }}: api
|
||||
{% endif -%}
|
||||
|
||||
# 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
|
||||
/: api
|
||||
/healthcheck: healthcheck
|
||||
{% if ingress_public.ingress_path -%}
|
||||
{{ ingress_public.ingress_path }}: api
|
||||
{% if heat.ingress_path -%}
|
||||
{{ heat.ingress_path }}: api
|
||||
{% endif -%}
|
||||
|
||||
# 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
|
||||
/: api
|
||||
/healthcheck: healthcheck
|
||||
{% if ingress_public.ingress_path -%}
|
||||
{{ ingress_public.ingress_path }}: api
|
||||
{% if heat.ingress_path -%}
|
||||
{{ heat.ingress_path }}: api
|
||||
{% endif -%}
|
||||
|
||||
# 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
|
||||
/: api
|
||||
/healthcheck: healthcheck
|
||||
{% if ingress_public.ingress_path -%}
|
||||
{{ ingress_public.ingress_path }}: api
|
||||
{% if heat.ingress_path -%}
|
||||
{{ heat.ingress_path }}: api
|
||||
{% endif -%}
|
||||
|
||||
# heat-api-cfn composite
|
||||
@ -54,8 +54,8 @@ paste.composite_factory = heat.api:root_app_factory
|
||||
paste.composite_factory = heat.api:root_app_factory
|
||||
/: api-cfn
|
||||
/healthcheck: healthcheck
|
||||
{% if ingress_public.ingress_path -%}
|
||||
{{ ingress_public.ingress_path }}: api-cfn
|
||||
{% if heat.ingress_path -%}
|
||||
{{ heat.ingress_path }}: api-cfn
|
||||
{% endif -%}
|
||||
|
||||
# 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
|
||||
/: api-cfn
|
||||
/healthcheck: healthcheck
|
||||
{% if ingress_public.ingress_path -%}
|
||||
{{ ingress_public.ingress_path }}: api-cfn
|
||||
{% if heat.ingress_path -%}
|
||||
{{ heat.ingress_path }}: api-cfn
|
||||
{% endif -%}
|
||||
|
||||
[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-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:
|
||||
- - traefik:ingress
|
||||
- keystone:ingress-internal
|
||||
@ -75,24 +65,9 @@ relations:
|
||||
- heat:identity-service
|
||||
- - keystone:identity-ops
|
||||
- heat:identity-ops
|
||||
- - traefik:ingress
|
||||
- heat:ingress-internal
|
||||
- - traefik-public:ingress
|
||||
- heat:ingress-public
|
||||
- - traefik:traefik-route
|
||||
- heat:traefik-route-internal
|
||||
- - traefik-public:traefik-route
|
||||
- heat:traefik-route-public
|
||||
- - rabbitmq: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:
|
||||
workload-status: active
|
||||
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
|
||||
|
||||
def add_complete_heat_shared_config_relation(
|
||||
self, harness: Harness
|
||||
) -> None:
|
||||
"""Add complete Heat shared config relation."""
|
||||
def add_complete_ingress_relation(self, harness: Harness) -> None:
|
||||
"""Add complete traefik-route relations."""
|
||||
harness.add_relation(
|
||||
"heat-config",
|
||||
"traefik-route-public",
|
||||
"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):
|
||||
@ -129,7 +132,7 @@ class TestHeatOperatorCharm(test_utils.CharmTestCase):
|
||||
|
||||
self.assertEqual(self.harness.charm.seen_events, [])
|
||||
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):
|
||||
"""Test all integrations for operator."""
|
||||
@ -148,9 +151,8 @@ class TestHeatOperatorCharm(test_utils.CharmTestCase):
|
||||
test_utils.set_all_pebbles_ready(self.harness)
|
||||
# this adds all the default/common relations
|
||||
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_heat_shared_config_relation(self.harness)
|
||||
|
||||
setup_cmds = [["heat-manage", "db_sync"]]
|
||||
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"]
|
||||
for f in config_files:
|
||||
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