From 0eb0eac2085dc2e6c922c24666bf933e80f09fd3 Mon Sep 17 00:00:00 2001 From: Hemanth Nakkina Date: Fri, 21 Jul 2023 08:54:07 +0530 Subject: [PATCH] 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 --- charms/heat-k8s/README.md | 6 +- charms/heat-k8s/TODO.txt | 3 - charms/heat-k8s/charmcraft.yaml | 4 +- charms/heat-k8s/config.yaml | 7 + charms/heat-k8s/fetch-libs.sh | 7 + .../v0/database_requires.py | 65 +++- .../keystone_k8s/v1/identity_service.py | 11 +- .../v1/kubernetes_service_patch.py | 341 ------------------ .../lib/charms/traefik_k8s/v1/ingress.py | 49 ++- charms/heat-k8s/metadata.yaml | 16 +- charms/heat-k8s/src/charm.py | 190 ++++++---- .../heat-k8s/src/templates/api-paste.ini.j2 | 82 ++++- charms/heat-k8s/src/templates/ceph.conf.j2 | 22 -- charms/heat-k8s/src/templates/heat.conf.j2 | 5 +- .../src/templates/parts/database-connection | 3 - .../src/templates/parts/identity-data | 10 - .../src/templates/parts/section-database | 6 +- .../src/templates/parts/section-identity | 24 +- charms/heat-k8s/tests/bundles/smoke.yaml | 26 +- charms/heat-k8s/tests/tests.yaml | 12 +- charms/heat-k8s/tests/unit/test_heat_charm.py | 2 +- 21 files changed, 363 insertions(+), 528 deletions(-) delete mode 100644 charms/heat-k8s/TODO.txt create mode 100755 charms/heat-k8s/fetch-libs.sh delete mode 100644 charms/heat-k8s/lib/charms/observability_libs/v1/kubernetes_service_patch.py delete mode 100644 charms/heat-k8s/src/templates/ceph.conf.j2 delete mode 100644 charms/heat-k8s/src/templates/parts/database-connection delete mode 100644 charms/heat-k8s/src/templates/parts/identity-data diff --git a/charms/heat-k8s/README.md b/charms/heat-k8s/README.md index a2156ee0..43c96657 100644 --- a/charms/heat-k8s/README.md +++ b/charms/heat-k8s/README.md @@ -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 diff --git a/charms/heat-k8s/TODO.txt b/charms/heat-k8s/TODO.txt deleted file mode 100644 index 2bd816ab..00000000 --- a/charms/heat-k8s/TODO.txt +++ /dev/null @@ -1,3 +0,0 @@ -* Register CFN endpoint with keystone traefik -* Tempest tests -* Switch to Antelope rocks diff --git a/charms/heat-k8s/charmcraft.yaml b/charms/heat-k8s/charmcraft.yaml index 5b0da209..ac49568b 100644 --- a/charms/heat-k8s/charmcraft.yaml +++ b/charms/heat-k8s/charmcraft.yaml @@ -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 diff --git a/charms/heat-k8s/config.yaml b/charms/heat-k8s/config.yaml index 606b5357..001d217d 100644 --- a/charms/heat-k8s/config.yaml +++ b/charms/heat-k8s/config.yaml @@ -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 diff --git a/charms/heat-k8s/fetch-libs.sh b/charms/heat-k8s/fetch-libs.sh new file mode 100755 index 00000000..482b8d07 --- /dev/null +++ b/charms/heat-k8s/fetch-libs.sh @@ -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 diff --git a/charms/heat-k8s/lib/charms/data_platform_libs/v0/database_requires.py b/charms/heat-k8s/lib/charms/data_platform_libs/v0/database_requires.py index 6f425e71..11ffd6ca 100644 --- a/charms/heat-k8s/lib/charms/data_platform_libs/v0/database_requires.py +++ b/charms/heat-k8s/lib/charms/data_platform_libs/v0/database_requires.py @@ -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 ) diff --git a/charms/heat-k8s/lib/charms/keystone_k8s/v1/identity_service.py b/charms/heat-k8s/lib/charms/keystone_k8s/v1/identity_service.py index 35556622..62dd9a3f 100644 --- a/charms/heat-k8s/lib/charms/keystone_k8s/v1/identity_service.py +++ b/charms/heat-k8s/lib/charms/keystone_k8s/v1/identity_service.py @@ -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 diff --git a/charms/heat-k8s/lib/charms/observability_libs/v1/kubernetes_service_patch.py b/charms/heat-k8s/lib/charms/observability_libs/v1/kubernetes_service_patch.py deleted file mode 100644 index 64dd13ce..00000000 --- a/charms/heat-k8s/lib/charms/observability_libs/v1/kubernetes_service_patch.py +++ /dev/null @@ -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() diff --git a/charms/heat-k8s/lib/charms/traefik_k8s/v1/ingress.py b/charms/heat-k8s/lib/charms/traefik_k8s/v1/ingress.py index b31a24d7..e393fb52 100644 --- a/charms/heat-k8s/lib/charms/traefik_k8s/v1/ingress.py +++ b/charms/heat-k8s/lib/charms/traefik_k8s/v1/ingress.py @@ -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) diff --git a/charms/heat-k8s/metadata.yaml b/charms/heat-k8s/metadata.yaml index 20cbaa86..677fa67f 100644 --- a/charms/heat-k8s/metadata.yaml +++ b/charms/heat-k8s/metadata.yaml @@ -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: diff --git a/charms/heat-k8s/src/charm.py b/charms/heat-k8s/src/charm.py index 269f8168..dc945c0d 100755 --- a/charms/heat-k8s/src/charm.py +++ b/charms/heat-k8s/src/charm.py @@ -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) diff --git a/charms/heat-k8s/src/templates/api-paste.ini.j2 b/charms/heat-k8s/src/templates/api-paste.ini.j2 index 09b4d82f..9b6e170b 100644 --- a/charms/heat-k8s/src/templates/api-paste.ini.j2 +++ b/charms/heat-k8s/src/templates/api-paste.ini.j2 @@ -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 diff --git a/charms/heat-k8s/src/templates/ceph.conf.j2 b/charms/heat-k8s/src/templates/ceph.conf.j2 deleted file mode 100644 index c293ae90..00000000 --- a/charms/heat-k8s/src/templates/ceph.conf.j2 +++ /dev/null @@ -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 %} diff --git a/charms/heat-k8s/src/templates/heat.conf.j2 b/charms/heat-k8s/src/templates/heat.conf.j2 index a53d370c..06229805 100644 --- a/charms/heat-k8s/src/templates/heat.conf.j2 +++ b/charms/heat-k8s/src/templates/heat.conf.j2 @@ -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 diff --git a/charms/heat-k8s/src/templates/parts/database-connection b/charms/heat-k8s/src/templates/parts/database-connection deleted file mode 100644 index 1fd70ce2..00000000 --- a/charms/heat-k8s/src/templates/parts/database-connection +++ /dev/null @@ -1,3 +0,0 @@ -{% if database.connection -%} -connection = {{ database.connection }} -{% endif -%} diff --git a/charms/heat-k8s/src/templates/parts/identity-data b/charms/heat-k8s/src/templates/parts/identity-data deleted file mode 100644 index 4b4af021..00000000 --- a/charms/heat-k8s/src/templates/parts/identity-data +++ /dev/null @@ -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 -%} diff --git a/charms/heat-k8s/src/templates/parts/section-database b/charms/heat-k8s/src/templates/parts/section-database index 986d9b10..eb52f65e 100644 --- a/charms/heat-k8s/src/templates/parts/section-database +++ b/charms/heat-k8s/src/templates/parts/section-database @@ -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 diff --git a/charms/heat-k8s/src/templates/parts/section-identity b/charms/heat-k8s/src/templates/parts/section-identity index 7568a9a4..cbb1d069 100644 --- a/charms/heat-k8s/src/templates/parts/section-identity +++ b/charms/heat-k8s/src/templates/parts/section-identity @@ -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 diff --git a/charms/heat-k8s/tests/bundles/smoke.yaml b/charms/heat-k8s/tests/bundles/smoke.yaml index 1b57e6bf..a6fc84db 100644 --- a/charms/heat-k8s/tests/bundles/smoke.yaml +++ b/charms/heat-k8s/tests/bundles/smoke.yaml @@ -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 + diff --git a/charms/heat-k8s/tests/tests.yaml b/charms/heat-k8s/tests/tests.yaml index b9d7d730..c8c062fb 100644 --- a/charms/heat-k8s/tests/tests.yaml +++ b/charms/heat-k8s/tests/tests.yaml @@ -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: '^.*$' diff --git a/charms/heat-k8s/tests/unit/test_heat_charm.py b/charms/heat-k8s/tests/unit/test_heat_charm.py index 48ca1904..f452a0b2 100644 --- a/charms/heat-k8s/tests/unit/test_heat_charm.py +++ b/charms/heat-k8s/tests/unit/test_heat_charm.py @@ -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."""