Move to 2023.1
* Move base to 22.04 * Use images from ghrc with tag 2023.1 * Fix heat-api service endpoints * Add healthchecks * Change api-paste file to reflect latest from upstream * Add ingress rules to api-paste.ini * Add service token role configs * Add tempest tests * Update necessary external libs * Make API service configurable using config parameter. With this change, heat-k8s charm can be used to deploy heat-api+heat-engine OR heat-api-cfn+heat-engine Change-Id: I3b89feba7f9b9d98e02b01cdb4177f7708c9d675
This commit is contained in:
parent
2ee5a5779a
commit
0eb0eac208
@ -10,10 +10,12 @@ Use links instead.
|
||||
|
||||
# heat-k8s
|
||||
|
||||
Charmhub package name: operator-template
|
||||
Charmhub package name: heat-k8s
|
||||
More information: https://charmhub.io/heat-k8s
|
||||
|
||||
Describe your charm in one or two sentences.
|
||||
Operator for OpenStack Heat. The charm can be used to deploy
|
||||
either heat-api service or heat-api-cnf service determined by
|
||||
the configuration parameter api_service.
|
||||
|
||||
## Other resources
|
||||
|
||||
|
@ -1,3 +0,0 @@
|
||||
* Register CFN endpoint with keystone traefik
|
||||
* Tempest tests
|
||||
* Switch to Antelope rocks
|
@ -2,10 +2,10 @@ type: "charm"
|
||||
bases:
|
||||
- build-on:
|
||||
- name: "ubuntu"
|
||||
channel: "20.04"
|
||||
channel: "22.04"
|
||||
run-on:
|
||||
- name: "ubuntu"
|
||||
channel: "20.04"
|
||||
channel: "22.04"
|
||||
parts:
|
||||
update-certificates:
|
||||
plugin: nil
|
||||
|
@ -25,3 +25,10 @@ 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
|
||||
|
7
charms/heat-k8s/fetch-libs.sh
Executable file
7
charms/heat-k8s/fetch-libs.sh
Executable file
@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "INFO: Fetching libs from charmhub."
|
||||
charmcraft fetch-lib charms.data_platform_libs.v0.database_requires
|
||||
charmcraft fetch-lib charms.keystone_k8s.v1.identity_service
|
||||
charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq
|
||||
charmcraft fetch-lib charms.traefik_k8s.v1.ingress
|
@ -1,4 +1,4 @@
|
||||
# Copyright 2022 Canonical Ltd.
|
||||
# Copyright 2023 Canonical Ltd.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@ -12,7 +12,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""[DEPRECATED] Relation 'requires' side abstraction for database relation.
|
||||
r"""[DEPRECATED] Relation 'requires' side abstraction for database relation.
|
||||
|
||||
This library is a uniform interface to a selection of common database
|
||||
metadata, with added custom events that add convenience to database management,
|
||||
@ -160,7 +160,7 @@ LIBAPI = 0
|
||||
|
||||
# Increment this PATCH version before using `charmcraft publish-lib` or reset
|
||||
# to 0 if you are raising the major API version.
|
||||
LIBPATCH = 5
|
||||
LIBPATCH = 6
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -171,16 +171,25 @@ class DatabaseEvent(RelationEvent):
|
||||
@property
|
||||
def endpoints(self) -> Optional[str]:
|
||||
"""Returns a comma separated list of read/write endpoints."""
|
||||
if not self.relation.app:
|
||||
return None
|
||||
|
||||
return self.relation.data[self.relation.app].get("endpoints")
|
||||
|
||||
@property
|
||||
def password(self) -> Optional[str]:
|
||||
"""Returns the password for the created user."""
|
||||
if not self.relation.app:
|
||||
return None
|
||||
|
||||
return self.relation.data[self.relation.app].get("password")
|
||||
|
||||
@property
|
||||
def read_only_endpoints(self) -> Optional[str]:
|
||||
"""Returns a comma separated list of read only endpoints."""
|
||||
if not self.relation.app:
|
||||
return None
|
||||
|
||||
return self.relation.data[self.relation.app].get("read-only-endpoints")
|
||||
|
||||
@property
|
||||
@ -189,16 +198,25 @@ class DatabaseEvent(RelationEvent):
|
||||
|
||||
MongoDB only.
|
||||
"""
|
||||
if not self.relation.app:
|
||||
return None
|
||||
|
||||
return self.relation.data[self.relation.app].get("replset")
|
||||
|
||||
@property
|
||||
def tls(self) -> Optional[str]:
|
||||
"""Returns whether TLS is configured."""
|
||||
if not self.relation.app:
|
||||
return None
|
||||
|
||||
return self.relation.data[self.relation.app].get("tls")
|
||||
|
||||
@property
|
||||
def tls_ca(self) -> Optional[str]:
|
||||
"""Returns TLS CA."""
|
||||
if not self.relation.app:
|
||||
return None
|
||||
|
||||
return self.relation.data[self.relation.app].get("tls-ca")
|
||||
|
||||
@property
|
||||
@ -207,11 +225,17 @@ class DatabaseEvent(RelationEvent):
|
||||
|
||||
MongoDB, Redis, OpenSearch and Kafka only.
|
||||
"""
|
||||
if not self.relation.app:
|
||||
return None
|
||||
|
||||
return self.relation.data[self.relation.app].get("uris")
|
||||
|
||||
@property
|
||||
def username(self) -> Optional[str]:
|
||||
"""Returns the created username."""
|
||||
if not self.relation.app:
|
||||
return None
|
||||
|
||||
return self.relation.data[self.relation.app].get("username")
|
||||
|
||||
@property
|
||||
@ -220,6 +244,9 @@ class DatabaseEvent(RelationEvent):
|
||||
|
||||
Version as informed by the database daemon.
|
||||
"""
|
||||
if not self.relation.app:
|
||||
return None
|
||||
|
||||
return self.relation.data[self.relation.app].get("version")
|
||||
|
||||
|
||||
@ -259,15 +286,15 @@ A tuple for storing the diff between two data mappings.
|
||||
class DatabaseRequires(Object):
|
||||
"""Requires-side of the database relation."""
|
||||
|
||||
on = DatabaseEvents()
|
||||
on = DatabaseEvents() # pyright: ignore [reportGeneralTypeIssues]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
charm,
|
||||
relation_name: str,
|
||||
database_name: str,
|
||||
extra_user_roles: str = None,
|
||||
relations_aliases: List[str] = None,
|
||||
extra_user_roles: Optional[str] = None,
|
||||
relations_aliases: Optional[List[str]] = None,
|
||||
):
|
||||
"""Manager of database client relations."""
|
||||
super().__init__(charm, relation_name)
|
||||
@ -352,9 +379,11 @@ class DatabaseRequires(Object):
|
||||
# Retrieve the old data from the data key in the local unit relation databag.
|
||||
old_data = json.loads(event.relation.data[self.local_unit].get("data", "{}"))
|
||||
# Retrieve the new data from the event relation databag.
|
||||
new_data = {
|
||||
key: value for key, value in event.relation.data[event.app].items() if key != "data"
|
||||
}
|
||||
new_data = (
|
||||
{key: value for key, value in event.relation.data[event.app].items() if key != "data"}
|
||||
if event.app
|
||||
else {}
|
||||
)
|
||||
|
||||
# These are the keys that were added to the databag and triggered this event.
|
||||
added = new_data.keys() - old_data.keys()
|
||||
@ -413,9 +442,11 @@ class DatabaseRequires(Object):
|
||||
"""
|
||||
data = {}
|
||||
for relation in self.relations:
|
||||
data[relation.id] = {
|
||||
key: value for key, value in relation.data[relation.app].items() if key != "data"
|
||||
}
|
||||
data[relation.id] = (
|
||||
{key: value for key, value in relation.data[relation.app].items() if key != "data"}
|
||||
if relation.app
|
||||
else {}
|
||||
)
|
||||
return data
|
||||
|
||||
def _update_relation_data(self, relation_id: int, data: dict) -> None:
|
||||
@ -461,7 +492,9 @@ class DatabaseRequires(Object):
|
||||
if "username" in diff.added and "password" in diff.added:
|
||||
# Emit the default event (the one without an alias).
|
||||
logger.info("database created at %s", datetime.now())
|
||||
self.on.database_created.emit(event.relation, app=event.app, unit=event.unit)
|
||||
getattr(self.on, "database_created").emit(
|
||||
event.relation, app=event.app, unit=event.unit
|
||||
)
|
||||
|
||||
# Emit the aliased event (if any).
|
||||
self._emit_aliased_event(event, "database_created")
|
||||
@ -475,7 +508,9 @@ class DatabaseRequires(Object):
|
||||
if "endpoints" in diff.added or "endpoints" in diff.changed:
|
||||
# Emit the default event (the one without an alias).
|
||||
logger.info("endpoints changed on %s", datetime.now())
|
||||
self.on.endpoints_changed.emit(event.relation, app=event.app, unit=event.unit)
|
||||
getattr(self.on, "endpoints_changed").emit(
|
||||
event.relation, app=event.app, unit=event.unit
|
||||
)
|
||||
|
||||
# Emit the aliased event (if any).
|
||||
self._emit_aliased_event(event, "endpoints_changed")
|
||||
@ -489,7 +524,7 @@ class DatabaseRequires(Object):
|
||||
if "read-only-endpoints" in diff.added or "read-only-endpoints" in diff.changed:
|
||||
# Emit the default event (the one without an alias).
|
||||
logger.info("read-only-endpoints changed on %s", datetime.now())
|
||||
self.on.read_only_endpoints_changed.emit(
|
||||
getattr(self.on, "read_only_endpoints_changed").emit(
|
||||
event.relation, app=event.app, unit=event.unit
|
||||
)
|
||||
|
||||
|
@ -100,7 +100,7 @@ LIBAPI = 1
|
||||
|
||||
# Increment this PATCH version before using `charmcraft publish-lib` or reset
|
||||
# to 0 if you are raising the major API version
|
||||
LIBPATCH = 0
|
||||
LIBPATCH = 1
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -349,6 +349,11 @@ class IdentityServiceRequires(Object):
|
||||
"""Return the public_auth_url."""
|
||||
return self.get_remote_app_data('public-auth-url')
|
||||
|
||||
@property
|
||||
def admin_role(self) -> str:
|
||||
"""Return the admin_role."""
|
||||
return self.get_remote_app_data('admin-role')
|
||||
|
||||
def register_services(self, service_endpoints: dict,
|
||||
region: str) -> None:
|
||||
"""Request access to the IdentityService server."""
|
||||
@ -481,7 +486,8 @@ class IdentityServiceProvides(Object):
|
||||
internal_auth_url: str,
|
||||
admin_auth_url: str,
|
||||
public_auth_url: str,
|
||||
service_credentials: str):
|
||||
service_credentials: str,
|
||||
admin_role: str):
|
||||
logging.debug("Setting identity_service connection information.")
|
||||
_identity_service_rel = None
|
||||
for relation in self.framework.model.relations[relation_name]:
|
||||
@ -516,3 +522,4 @@ class IdentityServiceProvides(Object):
|
||||
app_data["admin-auth-url"] = admin_auth_url
|
||||
app_data["public-auth-url"] = public_auth_url
|
||||
app_data["service-credentials"] = service_credentials
|
||||
app_data["admin-role"] = admin_role
|
||||
|
@ -1,341 +0,0 @@
|
||||
# Copyright 2021 Canonical Ltd.
|
||||
# See LICENSE file for licensing details.
|
||||
|
||||
"""# KubernetesServicePatch Library.
|
||||
|
||||
This library is designed to enable developers to more simply patch the Kubernetes Service created
|
||||
by Juju during the deployment of a sidecar charm. When sidecar charms are deployed, Juju creates a
|
||||
service named after the application in the namespace (named after the Juju model). This service by
|
||||
default contains a "placeholder" port, which is 65536/TCP.
|
||||
|
||||
When modifying the default set of resources managed by Juju, one must consider the lifecycle of the
|
||||
charm. In this case, any modifications to the default service (created during deployment), will be
|
||||
overwritten during a charm upgrade.
|
||||
|
||||
When initialised, this library binds a handler to the parent charm's `install` and `upgrade_charm`
|
||||
events which applies the patch to the cluster. This should ensure that the service ports are
|
||||
correct throughout the charm's life.
|
||||
|
||||
The constructor simply takes a reference to the parent charm, and a list of
|
||||
[`lightkube`](https://github.com/gtsystem/lightkube) ServicePorts that each define a port for the
|
||||
service. For information regarding the `lightkube` `ServicePort` model, please visit the
|
||||
`lightkube` [docs](https://gtsystem.github.io/lightkube-models/1.23/models/core_v1/#serviceport).
|
||||
|
||||
Optionally, a name of the service (in case service name needs to be patched as well), labels,
|
||||
selectors, and annotations can be provided as keyword arguments.
|
||||
|
||||
## Getting Started
|
||||
|
||||
To get started using the library, you just need to fetch the library using `charmcraft`. **Note
|
||||
that you also need to add `lightkube` and `lightkube-models` to your charm's `requirements.txt`.**
|
||||
|
||||
```shell
|
||||
cd some-charm
|
||||
charmcraft fetch-lib charms.observability_libs.v1.kubernetes_service_patch
|
||||
cat << EOF >> requirements.txt
|
||||
lightkube
|
||||
lightkube-models
|
||||
EOF
|
||||
```
|
||||
|
||||
Then, to initialise the library:
|
||||
|
||||
For `ClusterIP` services:
|
||||
|
||||
```python
|
||||
# ...
|
||||
from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch
|
||||
from lightkube.models.core_v1 import ServicePort
|
||||
|
||||
class SomeCharm(CharmBase):
|
||||
def __init__(self, *args):
|
||||
# ...
|
||||
port = ServicePort(443, name=f"{self.app.name}")
|
||||
self.service_patcher = KubernetesServicePatch(self, [port])
|
||||
# ...
|
||||
```
|
||||
|
||||
For `LoadBalancer`/`NodePort` services:
|
||||
|
||||
```python
|
||||
# ...
|
||||
from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch
|
||||
from lightkube.models.core_v1 import ServicePort
|
||||
|
||||
class SomeCharm(CharmBase):
|
||||
def __init__(self, *args):
|
||||
# ...
|
||||
port = ServicePort(443, name=f"{self.app.name}", targetPort=443, nodePort=30666)
|
||||
self.service_patcher = KubernetesServicePatch(
|
||||
self, [port], "LoadBalancer"
|
||||
)
|
||||
# ...
|
||||
```
|
||||
|
||||
Port protocols can also be specified. Valid protocols are `"TCP"`, `"UDP"`, and `"SCTP"`
|
||||
|
||||
```python
|
||||
# ...
|
||||
from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch
|
||||
from lightkube.models.core_v1 import ServicePort
|
||||
|
||||
class SomeCharm(CharmBase):
|
||||
def __init__(self, *args):
|
||||
# ...
|
||||
tcp = ServicePort(443, name=f"{self.app.name}-tcp", protocol="TCP")
|
||||
udp = ServicePort(443, name=f"{self.app.name}-udp", protocol="UDP")
|
||||
sctp = ServicePort(443, name=f"{self.app.name}-sctp", protocol="SCTP")
|
||||
self.service_patcher = KubernetesServicePatch(self, [tcp, udp, sctp])
|
||||
# ...
|
||||
```
|
||||
|
||||
Bound with custom events by providing `refresh_event` argument:
|
||||
For example, you would like to have a configurable port in your charm and want to apply
|
||||
service patch every time charm config is changed.
|
||||
|
||||
```python
|
||||
from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch
|
||||
from lightkube.models.core_v1 import ServicePort
|
||||
|
||||
class SomeCharm(CharmBase):
|
||||
def __init__(self, *args):
|
||||
# ...
|
||||
port = ServicePort(int(self.config["charm-config-port"]), name=f"{self.app.name}")
|
||||
self.service_patcher = KubernetesServicePatch(
|
||||
self,
|
||||
[port],
|
||||
refresh_event=self.on.config_changed
|
||||
)
|
||||
# ...
|
||||
```
|
||||
|
||||
Additionally, you may wish to use mocks in your charm's unit testing to ensure that the library
|
||||
does not try to make any API calls, or open any files during testing that are unlikely to be
|
||||
present, and could break your tests. The easiest way to do this is during your test `setUp`:
|
||||
|
||||
```python
|
||||
# ...
|
||||
|
||||
@patch("charm.KubernetesServicePatch", lambda x, y: None)
|
||||
def setUp(self, *unused):
|
||||
self.harness = Harness(SomeCharm)
|
||||
# ...
|
||||
```
|
||||
"""
|
||||
|
||||
import logging
|
||||
from types import MethodType
|
||||
from typing import List, Literal, Optional, Union
|
||||
|
||||
from lightkube import ApiError, Client
|
||||
from lightkube.core import exceptions
|
||||
from lightkube.models.core_v1 import ServicePort, ServiceSpec
|
||||
from lightkube.models.meta_v1 import ObjectMeta
|
||||
from lightkube.resources.core_v1 import Service
|
||||
from lightkube.types import PatchType
|
||||
from ops.charm import CharmBase
|
||||
from ops.framework import BoundEvent, Object
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# The unique Charmhub library identifier, never change it
|
||||
LIBID = "0042f86d0a874435adef581806cddbbb"
|
||||
|
||||
# Increment this major API version when introducing breaking changes
|
||||
LIBAPI = 1
|
||||
|
||||
# Increment this PATCH version before using `charmcraft publish-lib` or reset
|
||||
# to 0 if you are raising the major API version
|
||||
LIBPATCH = 7
|
||||
|
||||
ServiceType = Literal["ClusterIP", "LoadBalancer"]
|
||||
|
||||
|
||||
class KubernetesServicePatch(Object):
|
||||
"""A utility for patching the Kubernetes service set up by Juju."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
charm: CharmBase,
|
||||
ports: List[ServicePort],
|
||||
service_name: Optional[str] = None,
|
||||
service_type: ServiceType = "ClusterIP",
|
||||
additional_labels: Optional[dict] = None,
|
||||
additional_selectors: Optional[dict] = None,
|
||||
additional_annotations: Optional[dict] = None,
|
||||
*,
|
||||
refresh_event: Optional[Union[BoundEvent, List[BoundEvent]]] = None,
|
||||
):
|
||||
"""Constructor for KubernetesServicePatch.
|
||||
|
||||
Args:
|
||||
charm: the charm that is instantiating the library.
|
||||
ports: a list of ServicePorts
|
||||
service_name: allows setting custom name to the patched service. If none given,
|
||||
application name will be used.
|
||||
service_type: desired type of K8s service. Default value is in line with ServiceSpec's
|
||||
default value.
|
||||
additional_labels: Labels to be added to the kubernetes service (by default only
|
||||
"app.kubernetes.io/name" is set to the service name)
|
||||
additional_selectors: Selectors to be added to the kubernetes service (by default only
|
||||
"app.kubernetes.io/name" is set to the service name)
|
||||
additional_annotations: Annotations to be added to the kubernetes service.
|
||||
refresh_event: an optional bound event or list of bound events which
|
||||
will be observed to re-apply the patch (e.g. on port change).
|
||||
The `install` and `upgrade-charm` events would be observed regardless.
|
||||
"""
|
||||
super().__init__(charm, "kubernetes-service-patch")
|
||||
self.charm = charm
|
||||
self.service_name = service_name if service_name else self._app
|
||||
self.service = self._service_object(
|
||||
ports,
|
||||
service_name,
|
||||
service_type,
|
||||
additional_labels,
|
||||
additional_selectors,
|
||||
additional_annotations,
|
||||
)
|
||||
|
||||
# Make mypy type checking happy that self._patch is a method
|
||||
assert isinstance(self._patch, MethodType)
|
||||
# Ensure this patch is applied during the 'install' and 'upgrade-charm' events
|
||||
self.framework.observe(charm.on.install, self._patch)
|
||||
self.framework.observe(charm.on.upgrade_charm, self._patch)
|
||||
self.framework.observe(charm.on.update_status, self._patch)
|
||||
|
||||
# apply user defined events
|
||||
if refresh_event:
|
||||
if not isinstance(refresh_event, list):
|
||||
refresh_event = [refresh_event]
|
||||
|
||||
for evt in refresh_event:
|
||||
self.framework.observe(evt, self._patch)
|
||||
|
||||
def _service_object(
|
||||
self,
|
||||
ports: List[ServicePort],
|
||||
service_name: Optional[str] = None,
|
||||
service_type: ServiceType = "ClusterIP",
|
||||
additional_labels: Optional[dict] = None,
|
||||
additional_selectors: Optional[dict] = None,
|
||||
additional_annotations: Optional[dict] = None,
|
||||
) -> Service:
|
||||
"""Creates a valid Service representation.
|
||||
|
||||
Args:
|
||||
ports: a list of ServicePorts
|
||||
service_name: allows setting custom name to the patched service. If none given,
|
||||
application name will be used.
|
||||
service_type: desired type of K8s service. Default value is in line with ServiceSpec's
|
||||
default value.
|
||||
additional_labels: Labels to be added to the kubernetes service (by default only
|
||||
"app.kubernetes.io/name" is set to the service name)
|
||||
additional_selectors: Selectors to be added to the kubernetes service (by default only
|
||||
"app.kubernetes.io/name" is set to the service name)
|
||||
additional_annotations: Annotations to be added to the kubernetes service.
|
||||
|
||||
Returns:
|
||||
Service: A valid representation of a Kubernetes Service with the correct ports.
|
||||
"""
|
||||
if not service_name:
|
||||
service_name = self._app
|
||||
labels = {"app.kubernetes.io/name": self._app}
|
||||
if additional_labels:
|
||||
labels.update(additional_labels)
|
||||
selector = {"app.kubernetes.io/name": self._app}
|
||||
if additional_selectors:
|
||||
selector.update(additional_selectors)
|
||||
return Service(
|
||||
apiVersion="v1",
|
||||
kind="Service",
|
||||
metadata=ObjectMeta(
|
||||
namespace=self._namespace,
|
||||
name=service_name,
|
||||
labels=labels,
|
||||
annotations=additional_annotations, # type: ignore[arg-type]
|
||||
),
|
||||
spec=ServiceSpec(
|
||||
selector=selector,
|
||||
ports=ports,
|
||||
type=service_type,
|
||||
),
|
||||
)
|
||||
|
||||
def _patch(self, _) -> None:
|
||||
"""Patch the Kubernetes service created by Juju to map the correct port.
|
||||
|
||||
Raises:
|
||||
PatchFailed: if patching fails due to lack of permissions, or otherwise.
|
||||
"""
|
||||
try:
|
||||
client = Client()
|
||||
except exceptions.ConfigError as e:
|
||||
logger.warning("Error creating k8s client: %s", e)
|
||||
return
|
||||
|
||||
try:
|
||||
if self._is_patched(client):
|
||||
return
|
||||
if self.service_name != self._app:
|
||||
self._delete_and_create_service(client)
|
||||
client.patch(Service, self.service_name, self.service, patch_type=PatchType.MERGE)
|
||||
except ApiError as e:
|
||||
if e.status.code == 403:
|
||||
logger.error("Kubernetes service patch failed: `juju trust` this application.")
|
||||
else:
|
||||
logger.error("Kubernetes service patch failed: %s", str(e))
|
||||
else:
|
||||
logger.info("Kubernetes service '%s' patched successfully", self._app)
|
||||
|
||||
def _delete_and_create_service(self, client: Client):
|
||||
service = client.get(Service, self._app, namespace=self._namespace)
|
||||
service.metadata.name = self.service_name # type: ignore[attr-defined]
|
||||
service.metadata.resourceVersion = service.metadata.uid = None # type: ignore[attr-defined] # noqa: E501
|
||||
client.delete(Service, self._app, namespace=self._namespace)
|
||||
client.create(service)
|
||||
|
||||
def is_patched(self) -> bool:
|
||||
"""Reports if the service patch has been applied.
|
||||
|
||||
Returns:
|
||||
bool: A boolean indicating if the service patch has been applied.
|
||||
"""
|
||||
client = Client()
|
||||
return self._is_patched(client)
|
||||
|
||||
def _is_patched(self, client: Client) -> bool:
|
||||
# Get the relevant service from the cluster
|
||||
try:
|
||||
service = client.get(Service, name=self.service_name, namespace=self._namespace)
|
||||
except ApiError as e:
|
||||
if e.status.code == 404 and self.service_name != self._app:
|
||||
return False
|
||||
logger.error("Kubernetes service get failed: %s", str(e))
|
||||
raise
|
||||
|
||||
# Construct a list of expected ports, should the patch be applied
|
||||
expected_ports = [(p.port, p.targetPort) for p in self.service.spec.ports]
|
||||
# Construct a list in the same manner, using the fetched service
|
||||
fetched_ports = [
|
||||
(p.port, p.targetPort) for p in service.spec.ports # type: ignore[attr-defined]
|
||||
] # noqa: E501
|
||||
return expected_ports == fetched_ports
|
||||
|
||||
@property
|
||||
def _app(self) -> str:
|
||||
"""Name of the current Juju application.
|
||||
|
||||
Returns:
|
||||
str: A string containing the name of the current Juju application.
|
||||
"""
|
||||
return self.charm.app.name
|
||||
|
||||
@property
|
||||
def _namespace(self) -> str:
|
||||
"""The Kubernetes namespace we're running in.
|
||||
|
||||
Returns:
|
||||
str: A string containing the name of the current Kubernetes namespace.
|
||||
"""
|
||||
with open("/var/run/secrets/kubernetes.io/serviceaccount/namespace", "r") as f:
|
||||
return f.read().strip()
|
@ -69,7 +69,7 @@ LIBAPI = 1
|
||||
|
||||
# Increment this PATCH version before using `charmcraft publish-lib` or reset
|
||||
# to 0 if you are raising the major API version
|
||||
LIBPATCH = 12
|
||||
LIBPATCH = 15
|
||||
|
||||
DEFAULT_RELATION_NAME = "ingress"
|
||||
RELATION_INTERFACE = "ingress"
|
||||
@ -98,6 +98,7 @@ INGRESS_REQUIRES_APP_SCHEMA = {
|
||||
"host": {"type": "string"},
|
||||
"port": {"type": "string"},
|
||||
"strip-prefix": {"type": "string"},
|
||||
"redirect-https": {"type": "string"},
|
||||
},
|
||||
"required": ["model", "name", "host", "port"],
|
||||
}
|
||||
@ -113,12 +114,19 @@ INGRESS_PROVIDES_APP_SCHEMA = {
|
||||
try:
|
||||
from typing import TypedDict
|
||||
except ImportError:
|
||||
from typing_extensions import TypedDict # py35 compat
|
||||
from typing_extensions import TypedDict # py35 compatibility
|
||||
|
||||
# Model of the data a unit implementing the requirer will need to provide.
|
||||
RequirerData = TypedDict(
|
||||
"RequirerData",
|
||||
{"model": str, "name": str, "host": str, "port": int, "strip-prefix": bool},
|
||||
{
|
||||
"model": str,
|
||||
"name": str,
|
||||
"host": str,
|
||||
"port": int,
|
||||
"strip-prefix": bool,
|
||||
"redirect-https": bool,
|
||||
},
|
||||
total=False,
|
||||
)
|
||||
# Provider ingress data model.
|
||||
@ -148,7 +156,7 @@ class _IngressPerAppBase(Object):
|
||||
"""Base class for IngressPerUnit interface classes."""
|
||||
|
||||
def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME):
|
||||
super().__init__(charm, relation_name)
|
||||
super().__init__(charm, relation_name + "_V1")
|
||||
|
||||
self.charm: CharmBase = charm
|
||||
self.relation_name = relation_name
|
||||
@ -183,8 +191,8 @@ class _IngressPerAppBase(Object):
|
||||
|
||||
|
||||
class _IPAEvent(RelationEvent):
|
||||
__args__ = () # type: Tuple[str, ...]
|
||||
__optional_kwargs__ = {} # type: Dict[str, Any]
|
||||
__args__: Tuple[str, ...] = ()
|
||||
__optional_kwargs__: Dict[str, Any] = {}
|
||||
|
||||
@classmethod
|
||||
def __attrs__(cls):
|
||||
@ -226,14 +234,15 @@ class _IPAEvent(RelationEvent):
|
||||
class IngressPerAppDataProvidedEvent(_IPAEvent):
|
||||
"""Event representing that ingress data has been provided for an app."""
|
||||
|
||||
__args__ = ("name", "model", "port", "host", "strip_prefix")
|
||||
__args__ = ("name", "model", "port", "host", "strip_prefix", "redirect_https")
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
name = None # type: Optional[str]
|
||||
model = None # type: Optional[str]
|
||||
port = None # type: Optional[str]
|
||||
host = None # type: Optional[str]
|
||||
strip_prefix = False # type: bool
|
||||
name: Optional[str] = None
|
||||
model: Optional[str] = None
|
||||
port: Optional[str] = None
|
||||
host: Optional[str] = None
|
||||
strip_prefix: bool = False
|
||||
redirect_https: bool = False
|
||||
|
||||
|
||||
class IngressPerAppDataRemovedEvent(RelationEvent):
|
||||
@ -274,6 +283,7 @@ class IngressPerAppProvider(_IngressPerAppBase):
|
||||
data["port"],
|
||||
data["host"],
|
||||
data.get("strip-prefix", False),
|
||||
data.get("redirect-https", False),
|
||||
)
|
||||
|
||||
def _handle_relation_broken(self, event):
|
||||
@ -305,14 +315,15 @@ class IngressPerAppProvider(_IngressPerAppBase):
|
||||
return {}
|
||||
|
||||
databag = relation.data[relation.app]
|
||||
remote_data = {} # type: Dict[str, Union[int, str]]
|
||||
for k in ("port", "host", "model", "name", "mode", "strip-prefix"):
|
||||
remote_data: Dict[str, Union[int, str]] = {}
|
||||
for k in ("port", "host", "model", "name", "mode", "strip-prefix", "redirect-https"):
|
||||
v = databag.get(k)
|
||||
if v is not None:
|
||||
remote_data[k] = v
|
||||
_validate_data(remote_data, INGRESS_REQUIRES_APP_SCHEMA)
|
||||
remote_data["port"] = int(remote_data["port"])
|
||||
remote_data["strip-prefix"] = bool(remote_data.get("strip-prefix", False))
|
||||
remote_data["strip-prefix"] = bool(remote_data.get("strip-prefix", "false") == "true")
|
||||
remote_data["redirect-https"] = bool(remote_data.get("redirect-https", "false") == "true")
|
||||
return typing.cast(RequirerData, remote_data)
|
||||
|
||||
def get_data(self, relation: Relation) -> RequirerData: # type: ignore
|
||||
@ -387,7 +398,7 @@ class IngressPerAppReadyEvent(_IPAEvent):
|
||||
|
||||
__args__ = ("url",)
|
||||
if typing.TYPE_CHECKING:
|
||||
url = None # type: Optional[str]
|
||||
url: Optional[str] = None
|
||||
|
||||
|
||||
class IngressPerAppRevokedEvent(RelationEvent):
|
||||
@ -418,6 +429,7 @@ class IngressPerAppRequirer(_IngressPerAppBase):
|
||||
host: Optional[str] = None,
|
||||
port: Optional[int] = None,
|
||||
strip_prefix: bool = False,
|
||||
redirect_https: bool = False,
|
||||
):
|
||||
"""Constructor for IngressRequirer.
|
||||
|
||||
@ -433,6 +445,7 @@ class IngressPerAppRequirer(_IngressPerAppBase):
|
||||
host: Hostname to be used by the ingress provider to address the requiring
|
||||
application; if unspecified, the default Kubernetes service name will be used.
|
||||
strip_prefix: configure Traefik to strip the path prefix.
|
||||
redirect_https: redirect incoming requests to the HTTPS.
|
||||
|
||||
Request Args:
|
||||
port: the port of the service
|
||||
@ -441,6 +454,7 @@ class IngressPerAppRequirer(_IngressPerAppBase):
|
||||
self.charm: CharmBase = charm
|
||||
self.relation_name = relation_name
|
||||
self._strip_prefix = strip_prefix
|
||||
self._redirect_https = redirect_https
|
||||
|
||||
self._stored.set_default(current_url=None) # type: ignore
|
||||
|
||||
@ -517,6 +531,9 @@ class IngressPerAppRequirer(_IngressPerAppBase):
|
||||
if self._strip_prefix:
|
||||
data["strip-prefix"] = "true"
|
||||
|
||||
if self._redirect_https:
|
||||
data["redirect-https"] = "true"
|
||||
|
||||
_validate_data(data, INGRESS_REQUIRES_APP_SCHEMA)
|
||||
self.relation.data[self.app].update(data)
|
||||
|
||||
|
@ -11,7 +11,7 @@ bases:
|
||||
channel: 22.04/stable
|
||||
assumes:
|
||||
- k8s-api
|
||||
- juju >= 3.1
|
||||
- juju >= 3.2
|
||||
tags:
|
||||
- openstack
|
||||
source: https://opendev.org/openstack/charm-heat-k8s
|
||||
@ -20,8 +20,6 @@ issues: https://bugs.launchpad.net/charm-heat-k8s
|
||||
containers:
|
||||
heat-api:
|
||||
resource: heat-api-image
|
||||
heat-api-cfn:
|
||||
resource: heat-api-cfn-image
|
||||
heat-engine:
|
||||
resource: heat-engine-image
|
||||
|
||||
@ -29,17 +27,13 @@ resources:
|
||||
heat-api-image:
|
||||
type: oci-image
|
||||
description: OCI image for OpenStack Heat
|
||||
# docker.io/kolla/ubuntu-binary-heat-api:yoga
|
||||
upstream-source: docker.io/kolla/ubuntu-binary-heat-api@sha256:ca80d57606525facb404d8b0374701c02609c2ade5cb7e28ba132e666dd85949
|
||||
heat-api-cfn-image:
|
||||
type: oci-image
|
||||
description: OCI image for OpenStack Heat CFN
|
||||
# docker.io/kolla/ubuntu-binary-heat-api-cfn:yoga
|
||||
upstream-source: docker.io/kolla/ubuntu-binary-heat-api-cfn@sha256:6eec5915066b55696414022c86c42360cdbd4b8b1250e06b470fee25af394b66
|
||||
# ghcr.io/openstack-snaps/heat-api:2023.1
|
||||
upstream-source: ghcr.io/openstack-snaps/heat-api:2023.1
|
||||
heat-engine-image:
|
||||
type: oci-image
|
||||
description: OCI image for OpenStack Heat Engine
|
||||
upstream-source: docker.io/kolla/ubuntu-binary-heat-engine@sha256:a54491f7e09eedeaa42c046cedc478f8ba78fc455a6ba285a52a5d0f8ae1df84
|
||||
# ghcr.io/openstack-snaps/heat-engine:2023.1
|
||||
upstream-source: ghcr.io/openstack-snaps/heat-engine:2023.1
|
||||
|
||||
requires:
|
||||
database:
|
||||
|
@ -23,6 +23,7 @@ import secrets
|
||||
import string
|
||||
from typing import (
|
||||
List,
|
||||
Mapping,
|
||||
)
|
||||
|
||||
import ops_sunbeam.charm as sunbeam_charm
|
||||
@ -38,8 +39,8 @@ from ops.main import (
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
HEAT_API_CONTAINER = "heat-api"
|
||||
HEAT_API_CNF_CONTAINER = "heat-api-cfn"
|
||||
HEAT_ENGINE_CONTAINER = "heat-engine"
|
||||
HEAT_API_SERVICE_KEY = "api-service"
|
||||
|
||||
|
||||
class HeatAPIPebbleHandler(sunbeam_chandlers.ServicePebbleHandler):
|
||||
@ -51,44 +52,52 @@ class HeatAPIPebbleHandler(sunbeam_chandlers.ServicePebbleHandler):
|
||||
:returns: pebble service layer configuration for heat api service
|
||||
:rtype: dict
|
||||
"""
|
||||
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",
|
||||
}
|
||||
},
|
||||
}
|
||||
if self.charm.service_name == "heat-api-cfn":
|
||||
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",
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
def get_healthcheck_layer(self) -> dict:
|
||||
"""Health check pebble layer.
|
||||
|
||||
class HeatAPICFNPebbleHandler(sunbeam_chandlers.ServicePebbleHandler):
|
||||
"""Pebble handler for Heat API CNF container."""
|
||||
|
||||
def get_layer(self):
|
||||
"""Heat API CNF service.
|
||||
|
||||
:returns: pebble service layer configuration for API CNF service
|
||||
:rtype: dict
|
||||
:returns: pebble health check layer configuration for heat service
|
||||
"""
|
||||
return {
|
||||
"summary": "heat api cfn layer",
|
||||
"description": "pebble configuration for heat api cfn service",
|
||||
"services": {
|
||||
"heat-api-cfn": {
|
||||
"checks": {
|
||||
"online": {
|
||||
"override": "replace",
|
||||
"summary": "Heat API CNF",
|
||||
"command": "heat-api-cfn",
|
||||
"startup": "enabled",
|
||||
"user": "heat",
|
||||
"group": "heat",
|
||||
}
|
||||
},
|
||||
"level": "ready",
|
||||
"http": {
|
||||
"url": f"{self.charm.healthcheck_http_url}/healthcheck"
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -121,10 +130,9 @@ class HeatOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
||||
"""Charm the service."""
|
||||
|
||||
_state = StoredState()
|
||||
service_name = "heat-api"
|
||||
wsgi_admin_script = "/usr/bin/heat-wsgi-api"
|
||||
wsgi_public_script = "/usr/bin/heat-wsgi-api"
|
||||
heat_auth_encryption_key = "auth_encryption_key"
|
||||
heat_auth_encryption_key = "auth-encryption-key"
|
||||
|
||||
db_sync_cmds = [["heat-manage", "db_sync"]]
|
||||
|
||||
@ -148,14 +156,6 @@ class HeatOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
||||
self.template_dir,
|
||||
self.configure_charm,
|
||||
),
|
||||
HeatAPICFNPebbleHandler(
|
||||
self,
|
||||
HEAT_API_CNF_CONTAINER,
|
||||
"heat-api-cfn",
|
||||
self.default_container_configs(),
|
||||
self.template_dir,
|
||||
self.configure_charm,
|
||||
),
|
||||
HeatEnginePebbleHandler(
|
||||
self,
|
||||
HEAT_ENGINE_CONTAINER,
|
||||
@ -192,6 +192,50 @@ class HeatOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
||||
self.set_heat_auth_encryption_key()
|
||||
super().configure_charm(event)
|
||||
|
||||
def configure_app_leader(self, event):
|
||||
"""Configure app leader.
|
||||
|
||||
Ensure setting service_name in peer relation application data if it
|
||||
does not exist.
|
||||
"""
|
||||
super().configure_app_leader(event)
|
||||
|
||||
# Update service name in application data
|
||||
if not self.peers.get_app_data(HEAT_API_SERVICE_KEY):
|
||||
self.peers.set_app_data({HEAT_API_SERVICE_KEY: self.service_name})
|
||||
|
||||
@property
|
||||
def databases(self) -> Mapping[str, str]:
|
||||
"""Databases needed to support this charm.
|
||||
|
||||
Set database name as heat for both heat-api, heat-api-cfn.
|
||||
"""
|
||||
return {
|
||||
"database": "heat",
|
||||
}
|
||||
|
||||
@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.peers.get_app_data(HEAT_API_SERVICE_KEY)
|
||||
|
||||
if not service_name:
|
||||
service_name = self.config.get("api_service")
|
||||
if service_name not in ["heat-api", "heat-api-cfn"]:
|
||||
logger.warning(
|
||||
"Config parameter api_service should be one of heat-api, heat-api-cfn, defaulting to heat-api."
|
||||
)
|
||||
service_name = "heat-api"
|
||||
|
||||
return service_name
|
||||
|
||||
@property
|
||||
def service_conf(self) -> str:
|
||||
"""Service default configuration file."""
|
||||
@ -210,31 +254,28 @@ class HeatOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
||||
@property
|
||||
def service_endpoints(self):
|
||||
"""Return heat service endpoints."""
|
||||
return [
|
||||
{
|
||||
"service_name": "heat",
|
||||
"type": "heat",
|
||||
"description": "OpenStack Heat API",
|
||||
"internal_url": f"{self.internal_url}",
|
||||
"public_url": f"{self.public_url}",
|
||||
"admin_url": f"{self.admin_url}",
|
||||
}
|
||||
]
|
||||
|
||||
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": self.charm.healthcheck_http_url},
|
||||
if self.service_name == "heat-api-cfn":
|
||||
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",
|
||||
}
|
||||
]
|
||||
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",
|
||||
},
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
def default_container_configs(self):
|
||||
"""Return base container configs."""
|
||||
@ -249,9 +290,20 @@ class HeatOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
||||
|
||||
@property
|
||||
def default_public_ingress_port(self):
|
||||
"""Port for Heat AI service."""
|
||||
"""Port for Heat API service."""
|
||||
# Port 8000 if api service is heat-api-cfn
|
||||
if self.service_name == "heat-api-cfn":
|
||||
return 8000
|
||||
|
||||
# Default heat-api port
|
||||
return 8004
|
||||
|
||||
@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"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(HeatOperatorCharm)
|
||||
|
@ -1,9 +1,14 @@
|
||||
|
||||
# heat-api pipeline
|
||||
[pipeline:heat-api]
|
||||
pipeline = healthcheck cors request_id faultwrap http_proxy_to_wsgi versionnegotiation authurl authtoken context osprofiler apiv1app
|
||||
# heat-api composite
|
||||
[composite:heat-api]
|
||||
paste.composite_factory = heat.api:root_app_factory
|
||||
/: api
|
||||
/healthcheck: healthcheck
|
||||
{% if ingress_public.ingress_path -%}
|
||||
{{ ingress_public.ingress_path }}: api
|
||||
{% endif -%}
|
||||
|
||||
# heat-api pipeline for standalone heat
|
||||
# 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).
|
||||
@ -11,32 +16,69 @@ pipeline = healthcheck cors request_id faultwrap http_proxy_to_wsgi versionnegot
|
||||
# [paste_deploy]
|
||||
# flavor = standalone
|
||||
#
|
||||
[pipeline:heat-api-standalone]
|
||||
pipeline = healthcheck cors request_id faultwrap http_proxy_to_wsgi versionnegotiation authurl authpassword context apiv1app
|
||||
[composite:heat-api-standalone]
|
||||
paste.composite_factory = heat.api:root_app_factory
|
||||
/: api
|
||||
/healthcheck: healthcheck
|
||||
{% if ingress_public.ingress_path -%}
|
||||
{{ ingress_public.ingress_path }}: api
|
||||
{% endif -%}
|
||||
|
||||
# heat-api pipeline for custom cloud backends
|
||||
# heat-api composite for custom cloud backends
|
||||
# i.e. in heat.conf:
|
||||
# [paste_deploy]
|
||||
# flavor = custombackend
|
||||
#
|
||||
[pipeline:heat-api-custombackend]
|
||||
pipeline = healthcheck cors request_id context faultwrap versionnegotiation custombackendauth apiv1app
|
||||
[composite:heat-api-custombackend]
|
||||
paste.composite_factory = heat.api:root_app_factory
|
||||
/: api
|
||||
/healthcheck: healthcheck
|
||||
{% if ingress_public.ingress_path -%}
|
||||
{{ ingress_public.ingress_path }}: api
|
||||
{% endif -%}
|
||||
|
||||
# To enable, in heat.conf:
|
||||
# [paste_deploy]
|
||||
# flavor = noauth
|
||||
#
|
||||
[pipeline:heat-api-noauth]
|
||||
pipeline = healthcheck cors request_id faultwrap noauth context http_proxy_to_wsgi versionnegotiation apiv1app
|
||||
[composite:heat-api-noauth]
|
||||
paste.composite_factory = heat.api:root_app_factory
|
||||
/: api
|
||||
/healthcheck: healthcheck
|
||||
{% if ingress_public.ingress_path -%}
|
||||
{{ ingress_public.ingress_path }}: api
|
||||
{% endif -%}
|
||||
|
||||
# heat-api-cfn pipeline
|
||||
[pipeline:heat-api-cfn]
|
||||
pipeline = healthcheck cors request_id http_proxy_to_wsgi cfnversionnegotiation ec2authtoken authtoken context osprofiler apicfnv1app
|
||||
# heat-api-cfn composite
|
||||
[composite:heat-api-cfn]
|
||||
paste.composite_factory = heat.api:root_app_factory
|
||||
/: api-cfn
|
||||
/healthcheck: healthcheck
|
||||
{% if ingress_public.ingress_path -%}
|
||||
{{ ingress_public.ingress_path }}: api-cfn
|
||||
{% endif -%}
|
||||
|
||||
# heat-api-cfn pipeline for standalone heat
|
||||
# heat-api-cfn composite for standalone heat
|
||||
# relies exclusively on authenticating with ec2 signed requests
|
||||
[pipeline:heat-api-cfn-standalone]
|
||||
pipeline = healthcheck cors request_id http_proxy_to_wsgi cfnversionnegotiation ec2authtoken context apicfnv1app
|
||||
[composite:heat-api-cfn-standalone]
|
||||
paste.composite_factory = heat.api:root_app_factory
|
||||
/: api-cfn
|
||||
/healthcheck: healthcheck
|
||||
{% if ingress_public.ingress_path -%}
|
||||
{{ ingress_public.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
|
||||
@ -46,6 +88,9 @@ heat.app_factory = heat.api.openstack.v1:API
|
||||
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
|
||||
@ -100,6 +145,3 @@ paste.filter_factory = oslo_middleware.request_id:RequestId.factory
|
||||
|
||||
[filter:osprofiler]
|
||||
paste.filter_factory = osprofiler.web:WsgiMiddleware.factory
|
||||
|
||||
[filter:healthcheck]
|
||||
paste.filter_factory = oslo_middleware:Healthcheck.factory
|
||||
|
@ -1,22 +0,0 @@
|
||||
###############################################################################
|
||||
# [ WARNING ]
|
||||
# ceph configuration file maintained in aso
|
||||
# local changes may be overwritten.
|
||||
###############################################################################
|
||||
[global]
|
||||
{% if ceph.auth -%}
|
||||
auth_supported = {{ ceph.auth }}
|
||||
mon host = {{ ceph.mon_hosts }}
|
||||
{% endif -%}
|
||||
keyring = /etc/ceph/$cluster.$name.keyring
|
||||
log to syslog = false
|
||||
err to syslog = false
|
||||
clog to syslog = false
|
||||
{% if ceph.rbd_features %}
|
||||
rbd default features = {{ ceph.rbd_features }}
|
||||
{% endif %}
|
||||
|
||||
[client]
|
||||
{% if ceph_config.rbd_default_data_pool -%}
|
||||
rbd default data pool = {{ ceph_config.rbd_default_data_pool }}
|
||||
{% endif %}
|
@ -1,7 +1,6 @@
|
||||
[DEFAULT]
|
||||
debug = {{ options.debug }}
|
||||
|
||||
instance_user=ec2-user
|
||||
instance_driver=heat.engine.nova
|
||||
plugin_dirs = /usr/lib64/heat,/usr/lib/heat
|
||||
environment_dir=/etc/heat/environment.d
|
||||
@ -11,10 +10,10 @@ auth_encryption_key={{ peers.auth_encryption_key }}
|
||||
|
||||
transport_url = {{ amqp.transport_url }}
|
||||
|
||||
{% include "parts/section-database" %}
|
||||
|
||||
{% include "parts/section-identity" %}
|
||||
|
||||
[database]
|
||||
{% include "parts/database-connection" %}
|
||||
|
||||
[paste_deploy]
|
||||
api_paste_config=/etc/heat/api-paste.ini
|
||||
|
@ -1,3 +0,0 @@
|
||||
{% if database.connection -%}
|
||||
connection = {{ database.connection }}
|
||||
{% endif -%}
|
@ -1,10 +0,0 @@
|
||||
{% if identity_service.internal_host -%}
|
||||
www_authenticate_uri = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
|
||||
auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
|
||||
auth_type = password
|
||||
project_domain_name = {{ identity_service.service_domain_name }}
|
||||
user_domain_name = {{ identity_service.service_domain_name }}
|
||||
project_name = {{ identity_service.service_project_name }}
|
||||
username = {{ identity_service.service_user_name }}
|
||||
password = {{ identity_service.service_password }}
|
||||
{% endif -%}
|
@ -1,3 +1,7 @@
|
||||
[database]
|
||||
{% include "parts/database-connection" %}
|
||||
{% if database.connection -%}
|
||||
connection = {{ database.connection }}
|
||||
{% else -%}
|
||||
connection = sqlite:////var/lib/cinder/cinder.db
|
||||
{% endif -%}
|
||||
connection_recycle_time = 200
|
||||
|
@ -1,2 +1,24 @@
|
||||
[keystone_authtoken]
|
||||
{% include "parts/identity-data" %}
|
||||
{% if identity_service.admin_auth_url -%}
|
||||
auth_url = {{ identity_service.admin_auth_url }}
|
||||
interface = admin
|
||||
{% elif identity_service.internal_auth_url -%}
|
||||
auth_url = {{ identity_service.internal_auth_url }}
|
||||
interface = internal
|
||||
{% elif identity_service.internal_host -%}
|
||||
auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
|
||||
interface = internal
|
||||
{% endif -%}
|
||||
{% if identity_service.public_auth_url -%}
|
||||
www_authenticate_uri = {{ identity_service.public_auth_url }}
|
||||
{% elif identity_service.internal_host -%}
|
||||
www_authenticate_uri = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
|
||||
{% endif -%}
|
||||
auth_type = password
|
||||
project_domain_name = {{ identity_service.service_domain_name }}
|
||||
user_domain_name = {{ identity_service.service_domain_name }}
|
||||
project_name = {{ identity_service.service_project_name }}
|
||||
username = {{ identity_service.service_user_name }}
|
||||
password = {{ identity_service.service_password }}
|
||||
service_token_roles = {{ identity_service.admin_role }}
|
||||
service_token_roles_required = True
|
||||
|
@ -45,9 +45,18 @@ applications:
|
||||
scale: 1
|
||||
trust: true
|
||||
resources:
|
||||
heat-api-image: docker.io/kolla/ubuntu-binary-heat-api@sha256:ca80d57606525facb404d8b0374701c02609c2ade5cb7e28ba132e666dd85949
|
||||
heat-api-cfn-image: docker.io/kolla/ubuntu-binary-heat-api-cfn@sha256:6eec5915066b55696414022c86c42360cdbd4b8b1250e06b470fee25af394b66
|
||||
heat-engine-image: docker.io/kolla/ubuntu-binary-heat-engine@sha256:a54491f7e09eedeaa42c046cedc478f8ba78fc455a6ba285a52a5d0f8ae1df84
|
||||
heat-api-image: ghcr.io/openstack-snaps/heat-api:2023.1
|
||||
heat-engine-image: ghcr.io/openstack-snaps/heat-engine:2023.1
|
||||
|
||||
heat-cfn:
|
||||
charm: ../../heat-k8s.charm
|
||||
scale: 1
|
||||
trust: true
|
||||
options:
|
||||
api_service: heat-api-cfn
|
||||
resources:
|
||||
heat-api-image: ghcr.io/openstack-snaps/heat-api:2023.1
|
||||
heat-engine-image: ghcr.io/openstack-snaps/heat-engine:2023.1
|
||||
|
||||
relations:
|
||||
- - traefik:ingress
|
||||
@ -69,3 +78,14 @@ relations:
|
||||
- - rabbitmq:amqp
|
||||
- heat:amqp
|
||||
|
||||
- - mysql:database
|
||||
- heat-cfn:database
|
||||
- - keystone:identity-service
|
||||
- heat-cfn:identity-service
|
||||
- - traefik:ingress
|
||||
- heat-cfn:ingress-internal
|
||||
- - traefik-public:ingress
|
||||
- heat-cfn:ingress-public
|
||||
- - rabbitmq:amqp
|
||||
- heat-cfn:amqp
|
||||
|
||||
|
@ -4,15 +4,19 @@ smoke_bundles:
|
||||
- smoke
|
||||
# There is no storage provider at the moment so cannot run tests.
|
||||
configure:
|
||||
- zaza.charm_tests.noop.setup.basic_setup
|
||||
- zaza.openstack.charm_tests.keystone.setup.add_tempest_roles
|
||||
tests:
|
||||
- zaza.charm_tests.noop.tests.NoopTest
|
||||
- zaza.openstack.charm_tests.heat.tests.HeatTempestTestK8S
|
||||
tests_options:
|
||||
trust:
|
||||
- smoke
|
||||
ignore_hard_deploy_errors:
|
||||
- smoke
|
||||
|
||||
tempest:
|
||||
default:
|
||||
smoke: True
|
||||
|
||||
target_deploy_status:
|
||||
traefik:
|
||||
workload-status: active
|
||||
@ -32,4 +36,6 @@ target_deploy_status:
|
||||
heat:
|
||||
workload-status: active
|
||||
workload-status-message-regex: '^.*$'
|
||||
|
||||
heat-cfn:
|
||||
workload-status: active
|
||||
workload-status-message-regex: '^.*$'
|
||||
|
@ -76,7 +76,7 @@ class TestHeatOperatorCharm(test_utils.CharmTestCase):
|
||||
"""Test pebble ready handler."""
|
||||
self.assertEqual(self.harness.charm.seen_events, [])
|
||||
test_utils.set_all_pebbles_ready(self.harness)
|
||||
self.assertEqual(len(self.harness.charm.seen_events), 3)
|
||||
self.assertEqual(len(self.harness.charm.seen_events), 2)
|
||||
|
||||
def test_all_relations(self):
|
||||
"""Test all integrations for operator."""
|
||||
|
Loading…
x
Reference in New Issue
Block a user