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:
Hemanth Nakkina 2023-11-03 12:21:42 +05:30 committed by Guillaume Boutry
parent 9609359416
commit 4a473da7fe
No known key found for this signature in database
GPG Key ID: E95E3326872E55DE
14 changed files with 943 additions and 642 deletions

View File

@ -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

View File

@ -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

View File

@ -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")

View 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)

View File

@ -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:

View File

@ -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)

View 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

View File

@ -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]

View 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" %}

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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: '^.*$'

View File

@ -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)