diff --git a/charms/aodh-k8s/metadata.yaml b/charms/aodh-k8s/metadata.yaml index 4fbd6bbf..9d73eef4 100644 --- a/charms/aodh-k8s/metadata.yaml +++ b/charms/aodh-k8s/metadata.yaml @@ -72,6 +72,9 @@ requires: limit: 1 amqp: interface: rabbitmq + receive-ca-cert: + interface: certificate_transfer + optional: true provides: aodh: diff --git a/charms/aodh-k8s/src/charm.py b/charms/aodh-k8s/src/charm.py index 01f3b529..cc46e96b 100755 --- a/charms/aodh-k8s/src/charm.py +++ b/charms/aodh-k8s/src/charm.py @@ -93,7 +93,13 @@ class AODHEvaluatorPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): "root", "aodh", 0o640, - ) + ), + sunbeam_core.ContainerConfigFile( + "/usr/local/share/ca-certificates/ca-bundle.pem", + "root", + "aodh", + 0o640, + ), ] @@ -131,7 +137,13 @@ class AODHNotifierPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): "root", "aodh", 0o640, - ) + ), + sunbeam_core.ContainerConfigFile( + "/usr/local/share/ca-certificates/ca-bundle.pem", + "root", + "aodh", + 0o640, + ), ] @@ -169,7 +181,13 @@ class AODHListenerPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): "root", "aodh", 0o640, - ) + ), + sunbeam_core.ContainerConfigFile( + "/usr/local/share/ca-certificates/ca-bundle.pem", + "root", + "aodh", + 0o640, + ), ] @@ -209,7 +227,13 @@ class AODHExpirerPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): "root", "aodh", 0o640, - ) + ), + sunbeam_core.ContainerConfigFile( + "/usr/local/share/ca-certificates/ca-bundle.pem", + "root", + "aodh", + 0o640, + ), ] diff --git a/charms/barbican-k8s/metadata.yaml b/charms/barbican-k8s/metadata.yaml index e0f8d8ad..93935fab 100644 --- a/charms/barbican-k8s/metadata.yaml +++ b/charms/barbican-k8s/metadata.yaml @@ -42,6 +42,9 @@ requires: vault-kv: interface: vault-kv limit: 1 + receive-ca-cert: + interface: certificate_transfer + optional: true peers: peers: diff --git a/charms/barbican-k8s/src/charm.py b/charms/barbican-k8s/src/charm.py index 8d77b658..9e18c587 100755 --- a/charms/barbican-k8s/src/charm.py +++ b/charms/barbican-k8s/src/charm.py @@ -222,7 +222,13 @@ class BarbicanWorkerPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): return [ sunbeam_core.ContainerConfigFile( "/etc/barbican/barbican.conf", "barbican", "barbican" - ) + ), + sunbeam_core.ContainerConfigFile( + "/usr/local/share/ca-certificates/ca-bundle.pem", + "root", + "barbican", + 0o640, + ), ] @property @@ -464,10 +470,18 @@ class BarbicanVaultOperatorCharm(BarbicanOperatorCharm): def container_configs(self) -> List[sunbeam_core.ContainerConfigFile]: """Container configuration files for the service.""" _cconfigs = super().container_configs - _cconfigs.append( - sunbeam_core.ContainerConfigFile( - self.ca_crt_file, "barbican", "barbican" - ) + _cconfigs.extend( + [ + sunbeam_core.ContainerConfigFile( + self.ca_crt_file, "barbican", "barbican" + ), + sunbeam_core.ContainerConfigFile( + "/usr/local/share/ca-certificates/ca-bundle.pem", + "root", + "barbican", + 0o640, + ), + ] ) return _cconfigs diff --git a/charms/ceilometer-k8s/metadata.yaml b/charms/ceilometer-k8s/metadata.yaml index 53339441..637e31ae 100644 --- a/charms/ceilometer-k8s/metadata.yaml +++ b/charms/ceilometer-k8s/metadata.yaml @@ -49,6 +49,9 @@ requires: limit: 1 gnocchi-db: interface: gnocchi + receive-ca-cert: + interface: certificate_transfer + optional: true peers: peers: diff --git a/charms/ceilometer-k8s/src/charm.py b/charms/ceilometer-k8s/src/charm.py index 76b4174f..aecdea4b 100755 --- a/charms/ceilometer-k8s/src/charm.py +++ b/charms/ceilometer-k8s/src/charm.py @@ -305,6 +305,12 @@ class CeilometerOperatorCharm(sunbeam_charm.OSBaseOperatorCharmK8S): self.service_group, 0o640, ), + sunbeam_core.ContainerConfigFile( + "/usr/local/share/ca-certificates/ca-bundle.pem", + "root", + self.service_group, + 0o640, + ), ] return _cconfigs diff --git a/charms/cinder-k8s/metadata.yaml b/charms/cinder-k8s/metadata.yaml index 5f043c14..d0457d9a 100644 --- a/charms/cinder-k8s/metadata.yaml +++ b/charms/cinder-k8s/metadata.yaml @@ -57,6 +57,9 @@ requires: image-service: interface: glance optional: true + receive-ca-cert: + interface: certificate_transfer + optional: true peers: peers: diff --git a/charms/cinder-k8s/src/charm.py b/charms/cinder-k8s/src/charm.py index cb4da7e8..b4c48618 100755 --- a/charms/cinder-k8s/src/charm.py +++ b/charms/cinder-k8s/src/charm.py @@ -92,6 +92,12 @@ class CinderWSGIPebbleHandler(sunbeam_chandlers.WSGIPebbleHandler): sunbeam_core.ContainerConfigFile( "/etc/cinder/cinder.conf", "root", "cinder", 0o640 ), + sunbeam_core.ContainerConfigFile( + "/usr/local/share/ca-certificates/ca-bundle.pem", + "root", + "cinder", + 0o640, + ), ] @@ -143,7 +149,13 @@ class CinderSchedulerPebbleHandler(sunbeam_chandlers.PebbleHandler): return [ sunbeam_core.ContainerConfigFile( "/etc/cinder/cinder.conf", "root", "cinder", 0o640 - ) + ), + sunbeam_core.ContainerConfigFile( + "/usr/local/share/ca-certificates/ca-bundle.pem", + "root", + "cinder", + 0o640, + ), ] diff --git a/charms/designate-k8s/metadata.yaml b/charms/designate-k8s/metadata.yaml index d012d17f..c0027df0 100644 --- a/charms/designate-k8s/metadata.yaml +++ b/charms/designate-k8s/metadata.yaml @@ -45,6 +45,9 @@ requires: dns-backend: interface: bind-rndc limit: 1 + receive-ca-cert: + interface: certificate_transfer + optional: true peers: peers: diff --git a/charms/designate-k8s/src/charm.py b/charms/designate-k8s/src/charm.py index 708bc7a0..f059e0a0 100755 --- a/charms/designate-k8s/src/charm.py +++ b/charms/designate-k8s/src/charm.py @@ -144,6 +144,12 @@ class DesignatePebbleHandler(sunbeam_chandlers.WSGIPebbleHandler): "designate", "designate", ), + sunbeam_core.ContainerConfigFile( + "/usr/local/share/ca-certificates/ca-bundle.pem", + "root", + "designate", + 0o640, + ), ] ) return _cconfig diff --git a/charms/glance-k8s/metadata.yaml b/charms/glance-k8s/metadata.yaml index c0fabf3e..8f08e749 100644 --- a/charms/glance-k8s/metadata.yaml +++ b/charms/glance-k8s/metadata.yaml @@ -67,6 +67,9 @@ requires: ceph: interface: ceph-client optional: true + receive-ca-cert: + interface: certificate_transfer + optional: true provides: image-service: diff --git a/charms/glance-k8s/src/charm.py b/charms/glance-k8s/src/charm.py index 234b8dc8..3d444e4f 100755 --- a/charms/glance-k8s/src/charm.py +++ b/charms/glance-k8s/src/charm.py @@ -251,6 +251,12 @@ class GlanceOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): self.service_group, 0o640, ), + sunbeam_core.ContainerConfigFile( + "/usr/local/share/ca-certificates/ca-bundle.pem", + "root", + self.service_group, + 0o640, + ), ] if self.has_ceph_relation(): _cconfigs.extend( diff --git a/charms/gnocchi-k8s/metadata.yaml b/charms/gnocchi-k8s/metadata.yaml index d55f0315..fe885463 100644 --- a/charms/gnocchi-k8s/metadata.yaml +++ b/charms/gnocchi-k8s/metadata.yaml @@ -51,6 +51,9 @@ requires: limit: 1 ceph: interface: ceph-client + receive-ca-cert: + interface: certificate_transfer + optional: true provides: gnocchi-service: diff --git a/charms/gnocchi-k8s/src/charm.py b/charms/gnocchi-k8s/src/charm.py index e5909458..a3cd714e 100755 --- a/charms/gnocchi-k8s/src/charm.py +++ b/charms/gnocchi-k8s/src/charm.py @@ -280,6 +280,12 @@ class GnocchiOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): self.service_group, 0o640, ), + sunbeam_core.ContainerConfigFile( + "/usr/local/share/ca-certificates/ca-bundle.pem", + "root", + self.service_group, + 0o640, + ), ] def configure_app_leader(self, event: EventBase): diff --git a/charms/heat-k8s/metadata.yaml b/charms/heat-k8s/metadata.yaml index c74d2867..0a66f182 100644 --- a/charms/heat-k8s/metadata.yaml +++ b/charms/heat-k8s/metadata.yaml @@ -54,6 +54,9 @@ requires: interface: rabbitmq identity-ops: interface: keystone-resources + receive-ca-cert: + interface: certificate_transfer + optional: true peers: peers: diff --git a/charms/heat-k8s/src/charm.py b/charms/heat-k8s/src/charm.py index 625595e3..730ca4cb 100755 --- a/charms/heat-k8s/src/charm.py +++ b/charms/heat-k8s/src/charm.py @@ -426,6 +426,7 @@ class HeatOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): "rule": f"PathPrefix(`/{model}-{app}`)", "service": f"juju-{model}-{app}-service", "entryPoints": ["websecure"], + "tls": {}, }, } ) @@ -677,6 +678,12 @@ class HeatOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): self.service_group, 0o640, ), + sunbeam_core.ContainerConfigFile( + "/usr/local/share/ca-certificates/ca-bundle.pem", + "root", + self.service_group, + 0o640, + ), ] def heat_api_cfn_container_configs(self): @@ -694,6 +701,12 @@ class HeatOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): self.service_group, 0o640, ), + sunbeam_core.ContainerConfigFile( + "/usr/local/share/ca-certificates/ca-bundle.pem", + "root", + self.service_group, + 0o640, + ), ] def _get_create_role_ops(self) -> list: diff --git a/charms/horizon-k8s/metadata.yaml b/charms/horizon-k8s/metadata.yaml index 8e576263..fda37ef3 100644 --- a/charms/horizon-k8s/metadata.yaml +++ b/charms/horizon-k8s/metadata.yaml @@ -43,10 +43,13 @@ requires: identity-credentials: interface: keystone-credentials limit: 1 + receive-ca-cert: + interface: certificate_transfer + optional: true provides: horizon: - interface: horizon + interface: horizon peers: peers: diff --git a/charms/horizon-k8s/src/charm.py b/charms/horizon-k8s/src/charm.py index df11f88b..1be41d46 100755 --- a/charms/horizon-k8s/src/charm.py +++ b/charms/horizon-k8s/src/charm.py @@ -169,6 +169,22 @@ class HorizonOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): } ] + @property + def container_configs(self) -> List[sunbeam_core.ContainerConfigFile]: + """Container configuration files for the service.""" + _cconfigs = super().container_configs + _cconfigs.extend( + [ + sunbeam_core.ContainerConfigFile( + "/usr/local/share/ca-certificates/ca-bundle.pem", + "root", + self.service_group, + 0o640, + ), + ] + ) + return _cconfigs + def get_pebble_handlers(self) -> List[sunbeam_chandlers.PebbleHandler]: """Pebble handlers for the service.""" return [ diff --git a/charms/horizon-k8s/src/templates/local_settings.py.j2 b/charms/horizon-k8s/src/templates/local_settings.py.j2 index 301b1eef..75ffd9c6 100644 --- a/charms/horizon-k8s/src/templates/local_settings.py.j2 +++ b/charms/horizon-k8s/src/templates/local_settings.py.j2 @@ -198,6 +198,9 @@ OPENSTACK_KEYSTONE_URL = "{{ identity_credentials.internal_protocol }}://%s:{{ i OPENSTACK_API_VERSIONS = { "identity": 3, } OPENSTACK_KEYSTONE_MULTIDOMAIN_SUPPORT = True OPENSTACK_KEYSTONE_DEFAULT_DOMAIN = "{{ options.default_domain or identity_credentials.project_domain_id }}" +{% if receive_ca_cert and receive_ca_cert.ca_bundle -%} +OPENSTACK_SSL_CACERT = "/usr/local/share/ca-certificates/ca-bundle.pem" +{% endif -%} # Enables keystone web single-sign-on if set to True. #WEBSSO_ENABLED = False diff --git a/charms/keystone-k8s/actions.yaml b/charms/keystone-k8s/actions.yaml index b8b10d6a..d73153a6 100644 --- a/charms/keystone-k8s/actions.yaml +++ b/charms/keystone-k8s/actions.yaml @@ -27,3 +27,34 @@ regenerate-password: required: - username additionalProperties: False + +add-ca-certs: + description: | + Add CA certs for transfer + params: + name: + type: string + description: Name of CA certs bundle + ca: + type: string + description: Base64 encoded CA certificate + chain: + type: string + description: Base64 encoded CA Chain + required: + - name + - ca + additionalProperties: False +remove-ca-certs: + description: | + Remove CA certs + params: + name: + type: string + description: Name of CA certs bundle + required: + - name + additionalProperties: False +list-ca-certs: + description: | + List CA certs uploaded for transfer diff --git a/charms/keystone-k8s/metadata.yaml b/charms/keystone-k8s/metadata.yaml index 8cc88223..2635590b 100644 --- a/charms/keystone-k8s/metadata.yaml +++ b/charms/keystone-k8s/metadata.yaml @@ -29,6 +29,8 @@ provides: interface: keystone-credentials identity-ops: interface: keystone-resources + send-ca-cert: + interface: certificate_transfer requires: database: diff --git a/charms/keystone-k8s/src/charm.py b/charms/keystone-k8s/src/charm.py index 3cbe79e3..98c2c042 100755 --- a/charms/keystone-k8s/src/charm.py +++ b/charms/keystone-k8s/src/charm.py @@ -25,6 +25,8 @@ develop a new k8s charm using the Operator Framework: https://discourse.charmhub.io/t/4208 """ +import base64 +import binascii import json import logging from collections import ( @@ -54,6 +56,9 @@ import ops_sunbeam.guard as sunbeam_guard import ops_sunbeam.job_ctrl as sunbeam_job_ctrl import ops_sunbeam.relation_handlers as sunbeam_rhandlers import pwgen +from charms.certificate_transfer_interface.v0.certificate_transfer import ( + CertificateTransferProvides, +) from ops.charm import ( ActionEvent, RelationChangedEvent, @@ -68,10 +73,12 @@ from ops.model import ( ActiveStatus, MaintenanceStatus, ModelError, + Relation, SecretNotFoundError, SecretRotate, ) from utils import ( + certs, manager, ) @@ -81,8 +88,7 @@ KEYSTONE_CONTAINER = "keystone" FERNET_KEYS_PREFIX = "fernet-" CREDENTIALS_SECRET_PREFIX = "credentials_" SECRET_PREFIX = "secret://" - - +CERTIFICATE_TRANSFER_LABEL = "certs_to_transfer" KEYSTONE_CONF = "/etc/keystone/keystone.conf" LOGGING_CONF = "/etc/keystone/logging.conf" @@ -333,6 +339,7 @@ class KeystoneOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): IDSVC_RELATION_NAME = "identity-service" IDCREDS_RELATION_NAME = "identity-credentials" IDOPS_RELATION_NAME = "identity-ops" + SEND_CA_CERT_RELATION_NAME = "send-ca-cert" def __init__(self, framework): super().__init__(framework) @@ -344,9 +351,17 @@ class KeystoneOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): self._state.set_default(default_domain_id=None) self._state.set_default(service_project_id=None) + self.certificate_transfer = CertificateTransferProvides( + self, self.SEND_CA_CERT_RELATION_NAME + ) + self.framework.observe( self.on.peers_relation_changed, self._on_peer_data_changed ) + self.framework.observe( + self.on.send_ca_cert_relation_joined, + self._handle_certificate_transfer_on_event, + ) self.framework.observe( self.on.get_admin_password_action, self._get_admin_password_action ) @@ -361,6 +376,18 @@ class KeystoneOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): self.on.regenerate_password_action, self._regenerate_password_action, ) + self.framework.observe( + self.on.add_ca_certs_action, + self._add_ca_certs_action, + ) + self.framework.observe( + self.on.remove_ca_certs_action, + self._remove_ca_certs_action, + ) + self.framework.observe( + self.on.list_ca_certs_action, + self._list_ca_certs_action, + ) if self.bootstrapped(): self.bootstrap_status.set(ActiveStatus()) @@ -515,6 +542,108 @@ export OS_AUTH_VERSION=3 except Exception as e: event.fail(f"Regeneration of password failed: {e}") + def _create_certificate_transfer_secret( + self, name: str, ca_cert: str, chain_certs: str + ) -> bool: + certs_secret_id = self.peers.get_app_data(CERTIFICATE_TRANSFER_LABEL) + if certs_secret_id: + certs_secret = self.model.get_secret(id=certs_secret_id) + certificates = certs_secret.get_content() + certificates = json.loads(certificates.get("certs")) + if name in certificates: + return False + + certificates[name] = {"ca": ca_cert, "chain": chain_certs} + certs_secret.set_content({"certs": json.dumps(certificates)}) + else: + certificates = {} + certificates[name] = {"ca": ca_cert, "chain": chain_certs} + certificates = {"certs": json.dumps(certificates)} + certs_secret = self.model.app.add_secret( + certificates, label=CERTIFICATE_TRANSFER_LABEL + ) + self.peers.set_app_data( + {CERTIFICATE_TRANSFER_LABEL: certs_secret.id} + ) + + return True + + def _add_ca_certs_action(self, event: ActionEvent): + """Distribute CA certs.""" + if not self.unit.is_leader(): + event.fail("Please run action on lead unit.") + return + + name = event.params.get("name") + ca = event.params.get("ca") + chain = event.params.get("chain") + ca_cert = None + chain_certs = None + + try: + ca_bytes = base64.b64decode(ca) + ca_cert = ca_bytes.decode() + if not certs.certificate_is_valid(ca_bytes): + event.fail("Invalid CA certificate") + return + + if chain: + chain_bytes = base64.b64decode(chain) + chain_certs = chain_bytes.decode() + ca_chain_list = certs.parse_ca_chain(chain_bytes) + for _ca in ca_chain_list: + if not certs.certificate_is_valid(_ca): + event.fail("Invalid certificate in CA Chain") + return + + if not certs.ca_chain_is_valid(ca_chain_list): + event.fail("Invalid CA Chain") + except (binascii.Error, TypeError, ValueError) as e: + event.fail(str(e)) + return + + if not self._create_certificate_transfer_secret( + name, ca_cert, chain_certs + ): + event.fail("Certificate bundle already transferred") + + self._handle_certificate_transfers() + + def _remove_ca_certs_action(self, event: ActionEvent): + """Remove CA certs.""" + if not self.unit.is_leader(): + event.fail("Please run action on lead unit.") + return + + certs_secret_id = self.peers.get_app_data(CERTIFICATE_TRANSFER_LABEL) + if certs_secret_id: + certs_secret = self.model.get_secret(id=certs_secret_id) + certificates = certs_secret.get_content() + certificates = json.loads(certificates.get("certs")) + name = event.params.get("name") + if name not in certificates: + event.fail("Certificate bundle does not exist") + return + + certificates.pop(name) + certs_secret.set_content({"certs": json.dumps(certificates)}) + self._handle_certificate_transfers() + + def _list_ca_certs_action(self, event: ActionEvent): + """List CA certs.""" + if not self.unit.is_leader(): + event.fail("Please run action on lead unit.") + return + + certs_secret_id = self.peers.get_app_data(CERTIFICATE_TRANSFER_LABEL) + if certs_secret_id: + certs_secret = self.model.get_secret(id=certs_secret_id) + certificates = certs_secret.get_content() + certificates = json.loads(certificates.get("certs")) + event.set_results(certificates) + else: + event.set_results({}) + def _on_peer_data_changed(self, event: RelationChangedEvent): """Process fernet updates if possible.""" if self._state.unit_bootstrapped and self.is_leader_ready(): @@ -1585,6 +1714,84 @@ export OS_AUTH_VERSION=3 relation_id, relation_name, ops_response=response ) + def _get_combined_ca_and_chain(self, certs_secret=None) -> (str, list): + """Combine all certs for CA and chain. + + Action add-ca-certs allows to add multiple CA cert and chain certs. + Combine all CA certs in the secret and chains in the secret. + """ + if not certs_secret: + certs_secret_id = self.peers.get_app_data( + CERTIFICATE_TRANSFER_LABEL + ) + if not certs_secret_id: + logger.debug("No certificates to transfer") + return "", [] + + certs_secret = self.model.get_secret(id=certs_secret_id) + certificates = certs_secret.get_content() + certificates = json.loads(certificates.get("certs")) + + if not certificates: + logger.debug("No certificates to transfer") + return "", [] + + ca_list = [] + chain_list = [] + for name, bundle in certificates.items(): + _ca = bundle.get("ca") + _chain = bundle.get("chain") + if _ca: + ca_list.append(_ca) + if _chain: + chain_list.append(_chain) + + ca = "\n".join(ca_list) + # chain sent as list of single string containing complete chain + chain = [] + if chain: + chain = ["\n".join(chain_list)] + + return ca, chain + + def _handle_certificate_transfers( + self, relations: List[Relation] | None = None + ): + """Transfer certs on given relations. + + If relation is not specified, send on all the send-ca-cert + relations. + """ + if not relations: + relations = [ + relation + for relation in self.framework.model.relations[ + self.SEND_CA_CERT_RELATION_NAME + ] + ] + + ca, chain = self._get_combined_ca_and_chain() + + for relation in relations: + logger.debug( + "Transferring certificates for relation " + f"{relation.app.name} {relation.name}/{relation.id}" + ) + self.certificate_transfer.set_certificate( + certificate="", + ca=ca, + chain=chain, + relation_id=relation.id, + ) + + def _handle_certificate_transfer_on_event(self, event): + if not self.unit.is_leader(): + logger.debug("Skipping send ca cert as unit is not leader.") + return + + logger.debug(f"Handling send ca cert event: {event}") + self._handle_certificate_transfers([event.relation]) + if __name__ == "__main__": main(KeystoneOperatorCharm) diff --git a/charms/keystone-k8s/src/utils/certs.py b/charms/keystone-k8s/src/utils/certs.py new file mode 100644 index 00000000..a01751f8 --- /dev/null +++ b/charms/keystone-k8s/src/utils/certs.py @@ -0,0 +1,105 @@ +# Copyright 2024 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Helper functions to verify certificates.""" + +# Helper functions are picked from +# https://github.com/canonical/manual-tls-certificates-operator/blob/main/src/helpers.py + +import logging +import re +from typing import ( + List, +) + +from cryptography import ( + x509, +) +from cryptography.exceptions import ( + InvalidSignature, +) + +logger = logging.getLogger(__name__) + + +def certificate_is_valid(certificate: bytes) -> bool: + """Returns whether a certificate is valid. + + Args: + certificate: Certificate in bytes + + Returns: + bool: True/False + """ + try: + x509.load_pem_x509_certificate(certificate) + return True + except ValueError: + return False + + +def parse_ca_chain(ca_chain_pem: str) -> List[str]: + """Returns list of certificates based on a PEM CA Chain file. + + Args: + ca_chain_pem (str): String containing list of certificates. This string should look like: + -----BEGIN CERTIFICATE----- + + -----END CERTIFICATE----- + -----BEGIN CERTIFICATE----- + + -----END CERTIFICATE----- + + Returns: + list: List of certificates + """ + chain_list = re.findall( + pattern="(?=-----BEGIN CERTIFICATE-----)(.*?)(?<=-----END CERTIFICATE-----)", + string=ca_chain_pem, + flags=re.DOTALL, + ) + if not chain_list: + raise ValueError("No certificate found in chain file") + return chain_list + + +def ca_chain_is_valid(ca_chain: List[str]) -> bool: + """Returns whether a ca chain is valid. + + It uses the x509 certificate method verify_directly_issued_by, which checks + the certificate issuer name matches the issuer subject name and that + the certificate is signed by the issuer's private key. + + Args: + ca_chain: composed by a list of certificates. + + Returns: + whether the ca chain is valid. + """ + if len(ca_chain) < 2: + logger.warning( + "Invalid CA chain: It must contain at least 2 certificates." + ) + return False + for ca_cert, cert in zip(ca_chain, ca_chain[1:]): + try: + ca_cert_object = x509.load_pem_x509_certificate( + ca_cert.encode("utf-8") + ) + cert_object = x509.load_pem_x509_certificate(cert.encode("utf-8")) + cert_object.verify_directly_issued_by(ca_cert_object) + except (ValueError, TypeError, InvalidSignature) as e: + logger.warning("Invalid CA chain: %s", e) + return False + return True diff --git a/charms/magnum-k8s/metadata.yaml b/charms/magnum-k8s/metadata.yaml index 773d7688..69ed3f92 100644 --- a/charms/magnum-k8s/metadata.yaml +++ b/charms/magnum-k8s/metadata.yaml @@ -51,6 +51,9 @@ requires: limit: 1 amqp: interface: rabbitmq + receive-ca-cert: + interface: certificate_transfer + optional: true peers: peers: diff --git a/charms/magnum-k8s/src/charm.py b/charms/magnum-k8s/src/charm.py index ba32d9bd..9647adb4 100755 --- a/charms/magnum-k8s/src/charm.py +++ b/charms/magnum-k8s/src/charm.py @@ -115,6 +115,12 @@ class MagnumConductorPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): "magnum", "magnum", ), + sunbeam_core.ContainerConfigFile( + "/usr/local/share/ca-certificates/ca-bundle.pem", + "root", + "magnum", + 0o640, + ), ] @property @@ -232,6 +238,12 @@ class MagnumOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): "magnum", "magnum", ), + sunbeam_core.ContainerConfigFile( + "/usr/local/share/ca-certificates/ca-bundle.pem", + "root", + "magnum", + 0o640, + ), ] ) return _cconfigs diff --git a/charms/magnum-k8s/src/templates/magnum.conf.j2 b/charms/magnum-k8s/src/templates/magnum.conf.j2 index aa67c6da..f7f58973 100644 --- a/charms/magnum-k8s/src/templates/magnum.conf.j2 +++ b/charms/magnum-k8s/src/templates/magnum.conf.j2 @@ -13,7 +13,7 @@ db_auto_create = false {% include "parts/section-identity" %} [keystone_auth] -{% include "parts/identity-data" %} +auth_section = keystone_authtoken {% include "parts/section-service-user" %} @@ -32,3 +32,28 @@ region_name = RegionOne api_paste_config=/etc/magnum/api-paste.ini {% include "parts/section-oslo-messaging-rabbit" %} + +[glance_client] +{% if receive_ca_cert and receive_ca_cert.ca_bundle -%} +ca_file = /usr/local/share/ca-certificates/ca-bundle.pem +{% endif -%} + +[heat_client] +{% if receive_ca_cert and receive_ca_cert.ca_bundle -%} +ca_file = /usr/local/share/ca-certificates/ca-bundle.pem +{% endif -%} + +[neutron_client] +{% if receive_ca_cert and receive_ca_cert.ca_bundle -%} +ca_file = /usr/local/share/ca-certificates/ca-bundle.pem +{% endif -%} + +[nova_client] +{% if receive_ca_cert and receive_ca_cert.ca_bundle -%} +ca_file = /usr/local/share/ca-certificates/ca-bundle.pem +{% endif -%} + +[octavia_client] +{% if receive_ca_cert and receive_ca_cert.ca_bundle -%} +ca_file = /usr/local/share/ca-certificates/ca-bundle.pem +{% endif -%} diff --git a/charms/neutron-k8s/metadata.yaml b/charms/neutron-k8s/metadata.yaml index 126af4df..63cabc42 100644 --- a/charms/neutron-k8s/metadata.yaml +++ b/charms/neutron-k8s/metadata.yaml @@ -57,6 +57,9 @@ requires: certificates: interface: tls-certificates optional: true + receive-ca-cert: + interface: certificate_transfer + optional: true peers: peers: diff --git a/charms/neutron-k8s/src/charm.py b/charms/neutron-k8s/src/charm.py index b2edd8df..514ec412 100755 --- a/charms/neutron-k8s/src/charm.py +++ b/charms/neutron-k8s/src/charm.py @@ -91,6 +91,12 @@ class NeutronServerPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): sunbeam_core.ContainerConfigFile( "/etc/neutron/api-paste.ini", "neutron", "neutron" ), + sunbeam_core.ContainerConfigFile( + "/usr/local/share/ca-certificates/ca-bundle.pem", + "root", + "neutron", + 0o640, + ), ] @@ -235,6 +241,12 @@ class NeutronServerOVNPebbleHandler(NeutronServerPebbleHandler): sunbeam_core.ContainerConfigFile( "/etc/neutron/api-paste.ini", "root", "neutron", 0o640 ), + sunbeam_core.ContainerConfigFile( + "/usr/local/share/ca-certificates/ca-bundle.pem", + "root", + "neutron", + 0o640, + ), ] diff --git a/charms/nova-k8s/metadata.yaml b/charms/nova-k8s/metadata.yaml index 9fa8a124..c962ecc1 100644 --- a/charms/nova-k8s/metadata.yaml +++ b/charms/nova-k8s/metadata.yaml @@ -80,6 +80,9 @@ requires: interface: neutron-api placement: interface: placement + receive-ca-cert: + interface: certificate_transfer + optional: true provides: cloud-controller: diff --git a/charms/nova-k8s/src/charm.py b/charms/nova-k8s/src/charm.py index 3aef14d7..9537b819 100755 --- a/charms/nova-k8s/src/charm.py +++ b/charms/nova-k8s/src/charm.py @@ -101,7 +101,13 @@ class NovaSchedulerPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): "root", "nova", 0o640, - ) + ), + sunbeam_core.ContainerConfigFile( + "/usr/local/share/ca-certificates/ca-bundle.pem", + "root", + "nova", + 0o640, + ), ] @property @@ -148,7 +154,13 @@ class NovaConductorPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): "root", "nova", 0o640, - ) + ), + sunbeam_core.ContainerConfigFile( + "/usr/local/share/ca-certificates/ca-bundle.pem", + "root", + "nova", + 0o640, + ), ] @@ -375,6 +387,12 @@ class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): "nova", 0o640, ), + sunbeam_core.ContainerConfigFile( + "/usr/local/share/ca-certificates/ca-bundle.pem", + "root", + "nova", + 0o640, + ), sunbeam_core.ContainerConfigFile( "/root/cell_create_wrapper.sh", "root", "root", 0o755 ), diff --git a/charms/octavia-k8s/metadata.yaml b/charms/octavia-k8s/metadata.yaml index f2b5e65a..ad5bd52c 100644 --- a/charms/octavia-k8s/metadata.yaml +++ b/charms/octavia-k8s/metadata.yaml @@ -76,6 +76,9 @@ requires: identity-ops: interface: keystone-resources optional: true + receive-ca-cert: + interface: certificate_transfer + optional: true peers: peers: diff --git a/charms/octavia-k8s/src/charm.py b/charms/octavia-k8s/src/charm.py index 8f38ef2d..e24be0dc 100755 --- a/charms/octavia-k8s/src/charm.py +++ b/charms/octavia-k8s/src/charm.py @@ -221,6 +221,12 @@ class OctaviaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): self.service_group, 0o640, ), + sunbeam_core.ContainerConfigFile( + "/usr/local/share/ca-certificates/ca-bundle.pem", + "root", + self.service_group, + 0o640, + ), ] def handle_keystone_ops(self, event: ops.EventBase) -> None: diff --git a/charms/openstack-exporter-k8s/metadata.yaml b/charms/openstack-exporter-k8s/metadata.yaml index f234a767..6e342731 100644 --- a/charms/openstack-exporter-k8s/metadata.yaml +++ b/charms/openstack-exporter-k8s/metadata.yaml @@ -28,6 +28,9 @@ resources: requires: identity-ops: interface: keystone-resources + receive-ca-cert: + interface: certificate_transfer + optional: true provides: metrics-endpoint: diff --git a/charms/openstack-exporter-k8s/src/charm.py b/charms/openstack-exporter-k8s/src/charm.py index 8ee25467..e92cb2fb 100755 --- a/charms/openstack-exporter-k8s/src/charm.py +++ b/charms/openstack-exporter-k8s/src/charm.py @@ -161,6 +161,12 @@ class OSExporterOperatorCharm(sunbeam_charm.OSBaseOperatorCharmK8S): "_daemon_", "_daemon_", ), + sunbeam_core.ContainerConfigFile( + "/usr/local/share/ca-certificates/ca-bundle.pem", + "_daemon_", + "_daemon_", + 0o640, + ), ] @property diff --git a/charms/openstack-exporter-k8s/src/templates/clouds.yaml.j2 b/charms/openstack-exporter-k8s/src/templates/clouds.yaml.j2 index a5184208..197c953c 100644 --- a/charms/openstack-exporter-k8s/src/templates/clouds.yaml.j2 +++ b/charms/openstack-exporter-k8s/src/templates/clouds.yaml.j2 @@ -10,5 +10,8 @@ clouds: project_domain_name: {{ os_exporter.domain_name }} user_domain_name: {{ os_exporter.domain_name }} auth_url: {{ os_exporter.auth_url }} - # cacert: /etc/ssl/ca.pem +{% if receive_ca_cert and receive_ca_cert.ca_bundle -%} + cacert: {{ receive_ca_cert.ca_bundle }} +{% else -%} verify: false +{% endif -%} diff --git a/charms/placement-k8s/metadata.yaml b/charms/placement-k8s/metadata.yaml index b4c4ba9d..69aa5251 100644 --- a/charms/placement-k8s/metadata.yaml +++ b/charms/placement-k8s/metadata.yaml @@ -40,6 +40,10 @@ requires: interface: ingress optional: true limit: 1 + receive-ca-cert: + interface: certificate_transfer + optional: true + provides: placement: interface: placement diff --git a/charms/placement-k8s/src/charm.py b/charms/placement-k8s/src/charm.py index c3f33643..f1d7763d 100755 --- a/charms/placement-k8s/src/charm.py +++ b/charms/placement-k8s/src/charm.py @@ -95,6 +95,12 @@ class PlacementOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): self.service_group, 0o640, ), + sunbeam_core.ContainerConfigFile( + "/usr/local/share/ca-certificates/ca-bundle.pem", + "root", + self.service_group, + 0o640, + ), ] return _cconfigs diff --git a/common.sh b/common.sh index 72babea9..0d2060d3 100644 --- a/common.sh +++ b/common.sh @@ -77,6 +77,7 @@ EXTERNAL_AODH_LIBS=( "data_platform_libs" "rabbitmq_k8s" "traefik_k8s" + "certificate_transfer_interface" ) EXTERNAL_BARBICAN_LIBS=( @@ -84,10 +85,18 @@ EXTERNAL_BARBICAN_LIBS=( "rabbitmq_k8s" "traefik_k8s" "vault_k8s" + "certificate_transfer_interface" ) EXTERNAL_CEILOMETER_LIBS=( "rabbitmq_k8s" + "certificate_transfer_interface" +) + +EXTERNAL_CINDER_CEPH_LIBS=( + "data_platform_libs" + "rabbitmq_k8s" + "traefik_k8s" ) EXTERNAL_DESIGNATE_BIND_LIBS=( @@ -98,6 +107,7 @@ EXTERNAL_HEAT_LIBS=( "data_platform_libs" "rabbitmq_k8s" "traefik_route_k8s" + "certificate_transfer_interface" ) EXTERNAL_NEUTRON_LIBS=( @@ -105,18 +115,21 @@ EXTERNAL_NEUTRON_LIBS=( "rabbitmq_k8s" "traefik_k8s" "tls_certificates_interface" + "certificate_transfer_interface" ) EXTERNAL_OCTAVIA_LIBS=( "data_platform_libs" "traefik_k8s" "tls_certificates_interface" + "certificate_transfer_interface" ) EXTERNAL_OPENSTACK_EXPORTER_LIBS=( "grafana_k8s" "prometheus_k8s" "tls_certificates_interface" + "certificate_transfer_interface" ) EXTERNAL_OPENSTACK_HYPERVISOR_LIBS=( @@ -150,118 +163,134 @@ EXTERNAL_TEMPEST_LIBS=( # Config template parts for each component. CONFIG_TEMPLATES_AODH=( - "section-database" - "database-connection" - "section-identity" - "identity-data" - "section-oslo-messaging-rabbit" - "section-service-credentials" + "parts/section-database" + "parts/database-connection" + "parts/section-identity" + "parts/identity-data" + "parts/section-oslo-messaging-rabbit" + "parts/section-service-credentials" + "ca-bundle.pem.j2" ) CONFIG_TEMPLATES_BARBICAN=( - "section-identity" - "identity-data" - "section-oslo-messaging-rabbit" - "section-service-user" + "parts/section-identity" + "parts/identity-data" + "parts/section-oslo-messaging-rabbit" + "parts/section-service-user" + "ca-bundle.pem.j2" ) CONFIG_TEMPLATES_CEILOMETER=( - "identity-data-id-creds" - "section-oslo-messaging-rabbit" - "section-service-credentials-from-identity-service" - "section-service-user-from-identity-credentials" + "parts/identity-data-id-creds" + "parts/section-oslo-messaging-rabbit" + "parts/section-service-credentials-from-identity-service" + "parts/section-service-user-from-identity-credentials" + "ca-bundle.pem.j2" ) CONFIG_TEMPLATES_CINDER=( - "section-database" - "database-connection" - "section-identity" - "identity-data" - "section-oslo-messaging-rabbit" - "section-service-user" + "parts/section-database" + "parts/database-connection" + "parts/section-identity" + "parts/identity-data" + "parts/section-oslo-messaging-rabbit" + "parts/section-service-user" + "ca-bundle.pem.j2" ) CONFIG_TEMPLATES_CINDER_CEPH=( - "section-oslo-messaging-rabbit" - "section-oslo-notifications" + "parts/section-oslo-messaging-rabbit" + "parts/section-oslo-notifications" ) CONFIG_TEMPLATES_DESIGNATE=( - "database-connection" - "section-identity" - "identity-data" - "section-oslo-messaging-rabbit" - "section-service-user" + "parts/database-connection" + "parts/section-identity" + "parts/identity-data" + "parts/section-oslo-messaging-rabbit" + "parts/section-service-user" + "ca-bundle.pem.j2" ) CONFIG_TEMPLATES_GLANCE=( - "section-database" - "database-connection" - "section-identity" - "identity-data" - "section-oslo-messaging-rabbit" - "section-oslo-notifications" - "section-service-user" + "parts/section-database" + "parts/database-connection" + "parts/section-identity" + "parts/identity-data" + "parts/section-oslo-messaging-rabbit" + "parts/section-oslo-notifications" + "parts/section-service-user" + "ca-bundle.pem.j2" ) CONFIG_TEMPLATES_GNOCCHI=( - "database-connection" - "section-identity" - "identity-data" + "parts/database-connection" + "parts/section-identity" + "parts/identity-data" + "ca-bundle.pem.j2" ) CONFIG_TEMPLATES_HEAT=( - "section-database" - "database-connection" - "section-identity" - "identity-data" - "section-trustee" - "section-oslo-messaging-rabbit" + "parts/section-database" + "parts/database-connection" + "parts/section-identity" + "parts/identity-data" + "parts/section-trustee" + "parts/section-oslo-messaging-rabbit" + "ca-bundle.pem.j2" +) + +CONFIG_TEMPLATES_HORIZON=( + "ca-bundle.pem.j2" ) CONFIG_TEMPLATES_KEYSTONE=( - "section-database" - "database-connection" - "section-federation" - "section-middleware" - "section-oslo-cache" - "section-oslo-messaging-rabbit" - "section-oslo-middleware" - "section-oslo-notifications" - "section-signing" + "parts/section-database" + "parts/database-connection" + "parts/section-federation" + "parts/section-middleware" + "parts/section-oslo-cache" + "parts/section-oslo-messaging-rabbit" + "parts/section-oslo-middleware" + "parts/section-oslo-notifications" + "parts/section-signing" ) CONFIG_TEMPLATES_MAGNUM=( - "section-identity" - "identity-data" - "section-oslo-messaging-rabbit" - "section-service-user" - "section-trust" + "parts/section-identity" + "parts/identity-data" + "parts/section-oslo-messaging-rabbit" + "parts/section-service-user" + "parts/section-trust" + "ca-bundle.pem.j2" ) CONFIG_TEMPLATES_NEUTRON=( - "section-database" - "database-connection" - "section-identity" - "identity-data" - "section-oslo-messaging-rabbit" - "section-service-user" + "parts/section-database" + "parts/database-connection" + "parts/section-identity" + "parts/identity-data" + "parts/section-oslo-messaging-rabbit" + "parts/section-service-user" + "ca-bundle.pem.j2" ) CONFIG_TEMPLATES_NOVA=${CONFIG_TEMPLATES_NEUTRON[@]} CONFIG_TEMPLATES_OCTAVIA=( - "section-database" - "database-connection" - "section-identity" - "identity-data" + "parts/section-database" + "parts/database-connection" + "parts/section-identity" + "parts/identity-data" + "ca-bundle.pem.j2" ) CONFIG_TEMPLATES_PLACEMENT=( - "database-connection" - "section-identity" - "identity-data" - "section-service-user" + "parts/database-connection" + "parts/section-identity" + "parts/identity-data" + "parts/section-service-user" + "ca-bundle.pem.j2" ) declare -A INTERNAL_LIBS=( @@ -297,7 +326,7 @@ declare -A EXTERNAL_LIBS=( [barbican-k8s]=${EXTERNAL_BARBICAN_LIBS[@]} [ceilometer-k8s]=${EXTERNAL_CEILOMETER_LIBS[@]} [cinder-k8s]=${EXTERNAL_AODH_LIBS[@]} - [cinder-ceph-k8s]=${EXTERNAL_AODH_LIBS[@]} + [cinder-ceph-k8s]=${EXTERNAL_CINDER_CEPH_LIBS[@]} [designate-k8s]=${EXTERNAL_AODH_LIBS[@]} [designate-bind-k8s]=${EXTERNAL_DESIGNATE_BIND_LIBS[@]} [glance-k8s]=${EXTERNAL_AODH_LIBS[@]} @@ -331,14 +360,14 @@ declare -A CONFIG_TEMPLATES=( [glance-k8s]=${CONFIG_TEMPLATES_GLANCE[@]} [gnocchi-k8s]=${CONFIG_TEMPLATES_GNOCCHI[@]} [heat-k8s]=${CONFIG_TEMPLATES_HEAT[@]} - [horizon-k8s]=${NULL_ARRAY[@]} + [horizon-k8s]=${CONFIG_TEMPLATES_HORIZON[@]} [keystone-k8s]=${CONFIG_TEMPLATES_KEYSTONE[@]} [keystone-ldap-k8s]=${NULL_ARRAY[@]} [magnum-k8s]=${CONFIG_TEMPLATES_MAGNUM[@]} [neutron-k8s]=${CONFIG_TEMPLATES_NEUTRON[@]} [nova-k8s]=${CONFIG_TEMPLATES_NOVA[@]} [octavia-k8s]=${CONFIG_TEMPLATES_OCTAVIA[@]} - [openstack-exporter-k8s]=${NULL_ARRAY[@]} + [openstack-exporter-k8s]=${CONFIG_TEMPLATES_HORIZON[@]} [openstack-hypervisor]=${NULL_ARRAY[@]} [sunbeam-clusterd]=${NULL_ARRAY[@]} [sunbeam-machine]=${NULL_ARRAY[@]} @@ -376,7 +405,7 @@ function copy_config_templates { config_templates_=${CONFIG_TEMPLATES[$1]} for part in ${config_templates_[@]}; do echo "Copying $part" - cp -rf ../../templates/parts/$part src/templates/parts/ + cp -rf ../../templates/$part src/templates/$part done } @@ -392,6 +421,20 @@ function remove_libs { rm -rf lib } +function remove_config_templates { + echo "remove_config_templates for $1:" + config_templates_=${CONFIG_TEMPLATES[$1]} + for part in ${config_templates_[@]}; do + echo "Removing $part" + rm src/templates/$part + done + + if (test -d src/templates/parts) && (test -n "$(find src/templates/parts -maxdepth 0 -empty)") + then + remove_templates_parts_dir + fi +} + function remove_templates_parts_dir { rm -rf src/templates/parts } @@ -430,7 +473,7 @@ function pop_common_files { pushd charms/$1 remove_libs - remove_templates_parts_dir + remove_config_templates $1 remove_stestr_conf remove_juju_ignore diff --git a/libs/external/lib/charms/certificate_transfer_interface/v0/certificate_transfer.py b/libs/external/lib/charms/certificate_transfer_interface/v0/certificate_transfer.py new file mode 100644 index 00000000..44ddfdae --- /dev/null +++ b/libs/external/lib/charms/certificate_transfer_interface/v0/certificate_transfer.py @@ -0,0 +1,390 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + +"""Library for the certificate_transfer relation. + +This library contains the Requires and Provides classes for handling the +ertificate-transfer interface. + +## Getting Started +From a charm directory, fetch the library using `charmcraft`: + +```shell +charmcraft fetch-lib charms.certificate_transfer_interface.v0.certificate_transfer +``` + +### Provider charm +The provider charm is the charm providing public certificates to another charm that requires them. + +Example: +```python +from ops.charm import CharmBase, RelationJoinedEvent +from ops.main import main + +from lib.charms.certificate_transfer_interface.v0.certificate_transfer import CertificateTransferProvides # noqa: E501 W505 + + +class DummyCertificateTransferProviderCharm(CharmBase): + def __init__(self, *args): + super().__init__(*args) + self.certificate_transfer = CertificateTransferProvides(self, "certificates") + self.framework.observe( + self.on.certificates_relation_joined, self._on_certificates_relation_joined + ) + + def _on_certificates_relation_joined(self, event: RelationJoinedEvent): + certificate = "my certificate" + ca = "my CA certificate" + chain = ["certificate 1", "certificate 2"] + self.certificate_transfer.set_certificate(certificate=certificate, ca=ca, chain=chain, relation_id=event.relation.id) + + +if __name__ == "__main__": + main(DummyCertificateTransferProviderCharm) +``` + +### Requirer charm +The requirer charm is the charm requiring certificates from another charm that provides them. + +Example: +```python + +from ops.charm import CharmBase +from ops.main import main + +from lib.charms.certificate_transfer_interface.v0.certificate_transfer import ( + CertificateAvailableEvent, + CertificateRemovedEvent, + CertificateTransferRequires, +) + + +class DummyCertificateTransferRequirerCharm(CharmBase): + def __init__(self, *args): + super().__init__(*args) + self.certificate_transfer = CertificateTransferRequires(self, "certificates") + self.framework.observe( + self.certificate_transfer.on.certificate_available, self._on_certificate_available + ) + self.framework.observe( + self.certificate_transfer.on.certificate_removed, self._on_certificate_removed + ) + + def _on_certificate_available(self, event: CertificateAvailableEvent): + print(event.certificate) + print(event.ca) + print(event.chain) + print(event.relation_id) + + def _on_certificate_removed(self, event: CertificateRemovedEvent): + print(event.relation_id) + + +if __name__ == "__main__": + main(DummyCertificateTransferRequirerCharm) +``` + +You can relate both charms by running: + +```bash +juju relate +``` + +""" + + +import json +import logging +from typing import List + +from jsonschema import exceptions, validate # type: ignore[import-untyped] +from ops.charm import CharmBase, CharmEvents, RelationBrokenEvent, RelationChangedEvent +from ops.framework import EventBase, EventSource, Handle, Object + +# The unique Charmhub library identifier, never change it +LIBID = "3785165b24a743f2b0c60de52db25c8b" + +# Increment this major API version when introducing breaking changes +LIBAPI = 0 + +# Increment this PATCH version before using `charmcraft publish-lib` or reset +# to 0 if you are raising the major API version +LIBPATCH = 5 + +PYDEPS = ["jsonschema"] + + +logger = logging.getLogger(__name__) + + +PROVIDER_JSON_SCHEMA = { + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "https://canonical.github.io/charm-relation-interfaces/interfaces/certificate_transfer/schemas/provider.json", # noqa: E501 + "type": "object", + "title": "`certificate_transfer` provider schema", + "description": "The `certificate_transfer` root schema comprises the entire provider application databag for this interface.", # noqa: E501 + "default": {}, + "examples": [ + { + "certificate": "-----BEGIN CERTIFICATE-----\nMIIC6DCCAdCgAwIBAgIUW42TU9LSjEZLMCclWrvSwAsgRtcwDQYJKoZIhvcNAQEL\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIzMDMyNDE4\nNDMxOVoXDTI0MDMyMzE4NDMxOVowPDELMAkGA1UEAwwCb2sxLTArBgNVBC0MJGUw\nNjVmMWI3LTE2OWEtNDE5YS1iNmQyLTc3OWJkOGM4NzIwNjCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBAK42ixoklDH5K5i1NxXo/AFACDa956pE5RA57wlC\nBfgUYaIDRmv7TUVJh6zoMZSD6wjSZl3QgP7UTTZeHbvs3QE9HUwEkH1Lo3a8vD3z\neqsE2vSnOkpWWnPbfxiQyrTm77/LAWBt7lRLRLdfL6WcucD3wsGqm58sWXM3HG0f\nSN7PHCZUFqU6MpkHw8DiKmht5hBgWG+Vq3Zw8MNaqpwb/NgST3yYdcZwb58G2FTS\nZvDSdUfRmD/mY7TpciYV8EFylXNNFkth8oGNLunR9adgZ+9IunfRKj1a7S5GSwXU\nAZDaojw+8k5i3ikztsWH11wAVCiLj/3euIqq95z8xGycnKcCAwEAATANBgkqhkiG\n9w0BAQsFAAOCAQEAWMvcaozgBrZ/MAxzTJmp5gZyLxmMNV6iT9dcqbwzDtDtBvA/\n46ux6ytAQ+A7Bd3AubvozwCr1Id6g66ae0blWYRRZmF8fDdX/SBjIUkv7u9A3NVQ\nXN9gsEvK9pdpfN4ZiflfGSLdhM1STHycLmhG6H5s7HklbukMRhQi+ejbSzm/wiw1\nipcxuKhSUIVNkTLusN5b+HE2gwF1fn0K0z5jWABy08huLgbaEKXJEx5/FKLZGJga\nfpIzAdf25kMTu3gggseaAmzyX3AtT1i8A8nqYfe8fnnVMkvud89kq5jErv/hlMC9\n49g5yWQR2jilYYM3j9BHDuB+Rs+YS5BCep1JnQ==\n-----END CERTIFICATE-----\n", # noqa: E501 + "ca": "-----BEGIN CERTIFICATE-----\nMIIC6DCCAdCgAwIBAgIUdiBwE/CtaBXJl3MArjZen6Y8kigwDQYJKoZIhvcNAQEL\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIzMDMyNDE4\nNDg1OVoXDTI0MDMyMzE4NDg1OVowPDELMAkGA1UEAwwCb2sxLTArBgNVBC0MJDEw\nMDdjNDBhLWUwYzMtNDVlOS05YTAxLTVlYjY0NWQ0ZmEyZDCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBANOnUl6JDlXpLMRr/PxgtfE/E5Yk6E/TkPkPL/Kk\ntUGjEi42XZDg9zn3U6cjTDYu+rfKY2jiitfsduW6DQIkEpz3AvbuCMbbgnFpcjsB\nYysLSMTmuz/AVPrfnea/tQTALcONCSy1VhAjGSr81ZRSMB4khl9StSauZrbkpJ1P\nshqkFSUyAi31mKrnXz0Es/v0Yi0FzAlgWrZ4u1Ld+Bo2Xz7oK4mHf7/93Jc+tEaM\nIqG6ocD0q8bjPp0tlSxftVADNUzWlZfM6fue5EXzOsKqyDrxYOSchfU9dNzKsaBX\nkxbHEeSUPJeYYj7aVPEfAs/tlUGsoXQvwWfRie8grp2BoLECAwEAATANBgkqhkiG\n9w0BAQsFAAOCAQEACZARBpHYH6Gr2a1ka0mCWfBmOZqfDVan9rsI5TCThoylmaXW\nquEiZ2LObI+5faPzxSBhr9TjJlQamsd4ywout7pHKN8ZGqrCMRJ1jJbUfobu1n2k\nUOsY4+jzV1IRBXJzj64fLal4QhUNv341lAer6Vz3cAyRk7CK89b/DEY0x+jVpyZT\n1osx9JtsOmkDTgvdStGzq5kPKWOfjwHkmKQaZXliCgqbhzcCERppp1s/sX6K7nIh\n4lWiEmzUSD3Hngk51KGWlpZszO5KQ4cSZ3HUt/prg+tt0ROC3pY61k+m5dDUa9M8\nRtMI6iTjzSj/UV8DiAx0yeM+bKoy4jGeXmaL3g==\n-----END CERTIFICATE-----\n", # noqa: E501 + "chain": [ + "-----BEGIN CERTIFICATE-----\nMIIC6DCCAdCgAwIBAgIUW42TU9LSjEZLMCclWrvSwAsgRtcwDQYJKoZIhvcNAQEL\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIzMDMyNDE4\nNDMxOVoXDTI0MDMyMzE4NDMxOVowPDELMAkGA1UEAwwCb2sxLTArBgNVBC0MJGUw\nNjVmMWI3LTE2OWEtNDE5YS1iNmQyLTc3OWJkOGM4NzIwNjCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBAK42ixoklDH5K5i1NxXo/AFACDa956pE5RA57wlC\nBfgUYaIDRmv7TUVJh6zoMZSD6wjSZl3QgP7UTTZeHbvs3QE9HUwEkH1Lo3a8vD3z\neqsE2vSnOkpWWnPbfxiQyrTm77/LAWBt7lRLRLdfL6WcucD3wsGqm58sWXM3HG0f\nSN7PHCZUFqU6MpkHw8DiKmht5hBgWG+Vq3Zw8MNaqpwb/NgST3yYdcZwb58G2FTS\nZvDSdUfRmD/mY7TpciYV8EFylXNNFkth8oGNLunR9adgZ+9IunfRKj1a7S5GSwXU\nAZDaojw+8k5i3ikztsWH11wAVCiLj/3euIqq95z8xGycnKcCAwEAATANBgkqhkiG\n9w0BAQsFAAOCAQEAWMvcaozgBrZ/MAxzTJmp5gZyLxmMNV6iT9dcqbwzDtDtBvA/\n46ux6ytAQ+A7Bd3AubvozwCr1Id6g66ae0blWYRRZmF8fDdX/SBjIUkv7u9A3NVQ\nXN9gsEvK9pdpfN4ZiflfGSLdhM1STHycLmhG6H5s7HklbukMRhQi+ejbSzm/wiw1\nipcxuKhSUIVNkTLusN5b+HE2gwF1fn0K0z5jWABy08huLgbaEKXJEx5/FKLZGJga\nfpIzAdf25kMTu3gggseaAmzyX3AtT1i8A8nqYfe8fnnVMkvud89kq5jErv/hlMC9\n49g5yWQR2jilYYM3j9BHDuB+Rs+YS5BCep1JnQ==\n-----END CERTIFICATE-----\n", # noqa: E501 + "-----BEGIN CERTIFICATE-----\nMIIC6DCCAdCgAwIBAgIUdiBwE/CtaBXJl3MArjZen6Y8kigwDQYJKoZIhvcNAQEL\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIzMDMyNDE4\nNDg1OVoXDTI0MDMyMzE4NDg1OVowPDELMAkGA1UEAwwCb2sxLTArBgNVBC0MJDEw\nMDdjNDBhLWUwYzMtNDVlOS05YTAxLTVlYjY0NWQ0ZmEyZDCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBANOnUl6JDlXpLMRr/PxgtfE/E5Yk6E/TkPkPL/Kk\ntUGjEi42XZDg9zn3U6cjTDYu+rfKY2jiitfsduW6DQIkEpz3AvbuCMbbgnFpcjsB\nYysLSMTmuz/AVPrfnea/tQTALcONCSy1VhAjGSr81ZRSMB4khl9StSauZrbkpJ1P\nshqkFSUyAi31mKrnXz0Es/v0Yi0FzAlgWrZ4u1Ld+Bo2Xz7oK4mHf7/93Jc+tEaM\nIqG6ocD0q8bjPp0tlSxftVADNUzWlZfM6fue5EXzOsKqyDrxYOSchfU9dNzKsaBX\nkxbHEeSUPJeYYj7aVPEfAs/tlUGsoXQvwWfRie8grp2BoLECAwEAATANBgkqhkiG\n9w0BAQsFAAOCAQEACZARBpHYH6Gr2a1ka0mCWfBmOZqfDVan9rsI5TCThoylmaXW\nquEiZ2LObI+5faPzxSBhr9TjJlQamsd4ywout7pHKN8ZGqrCMRJ1jJbUfobu1n2k\nUOsY4+jzV1IRBXJzj64fLal4QhUNv341lAer6Vz3cAyRk7CK89b/DEY0x+jVpyZT\n1osx9JtsOmkDTgvdStGzq5kPKWOfjwHkmKQaZXliCgqbhzcCERppp1s/sX6K7nIh\n4lWiEmzUSD3Hngk51KGWlpZszO5KQ4cSZ3HUt/prg+tt0ROC3pY61k+m5dDUa9M8\nRtMI6iTjzSj/UV8DiAx0yeM+bKoy4jGeXmaL3g==\n-----END CERTIFICATE-----\n", # noqa: E501 + ], + } + ], + "properties": { + "certificate": { + "$id": "#/properties/certificate", + "type": "string", + "title": "Public TLS certificate", + "description": "Public TLS certificate", + }, + "ca": { + "$id": "#/properties/ca", + "type": "string", + "title": "CA public TLS certificate", + "description": "CA Public TLS certificate", + }, + "chain": { + "$id": "#/properties/chain", + "type": "array", + "items": {"type": "string", "$id": "#/properties/chain/items"}, + "title": "CA public TLS certificate chain", + "description": "CA public TLS certificate chain", + }, + }, + "anyOf": [{"required": ["certificate"]}, {"required": ["ca"]}, {"required": ["chain"]}], + "additionalProperties": True, +} + + +class CertificateAvailableEvent(EventBase): + """Charm Event triggered when a TLS certificate is available.""" + + def __init__( + self, + handle: Handle, + certificate: str, + ca: str, + chain: List[str], + relation_id: int, + ): + super().__init__(handle) + self.certificate = certificate + self.ca = ca + self.chain = chain + self.relation_id = relation_id + + def snapshot(self) -> dict: + """Return snapshot.""" + return { + "certificate": self.certificate, + "ca": self.ca, + "chain": self.chain, + "relation_id": self.relation_id, + } + + def restore(self, snapshot: dict): + """Restores snapshot.""" + self.certificate = snapshot["certificate"] + self.ca = snapshot["ca"] + self.chain = snapshot["chain"] + self.relation_id = snapshot["relation_id"] + + +class CertificateRemovedEvent(EventBase): + """Charm Event triggered when a TLS certificate is removed.""" + + def __init__(self, handle: Handle, relation_id: int): + super().__init__(handle) + self.relation_id = relation_id + + def snapshot(self) -> dict: + """Return snapshot.""" + return {"relation_id": self.relation_id} + + def restore(self, snapshot: dict): + """Restores snapshot.""" + self.relation_id = snapshot["relation_id"] + + +def _load_relation_data(raw_relation_data: dict) -> dict: + """Load relation data from the relation data bag. + + Args: + raw_relation_data: Relation data from the databag + + Returns: + dict: Relation data in dict format. + """ + loaded_relation_data = {} + for key in raw_relation_data: + try: + loaded_relation_data[key] = json.loads(raw_relation_data[key]) + except (json.decoder.JSONDecodeError, TypeError): + loaded_relation_data[key] = raw_relation_data[key] + return loaded_relation_data + + +class CertificateTransferRequirerCharmEvents(CharmEvents): + """List of events that the Certificate Transfer requirer charm can leverage.""" + + certificate_available = EventSource(CertificateAvailableEvent) + certificate_removed = EventSource(CertificateRemovedEvent) + + +class CertificateTransferProvides(Object): + """Certificate Transfer provider class.""" + + def __init__(self, charm: CharmBase, relationship_name: str): + super().__init__(charm, relationship_name) + self.charm = charm + self.relationship_name = relationship_name + + def set_certificate( + self, + certificate: str, + ca: str, + chain: List[str], + relation_id: int, + ) -> None: + """Add certificates to relation data. + + Args: + certificate (str): Certificate + ca (str): CA Certificate + chain (list): CA Chain + relation_id (int): Juju relation ID + + Returns: + None + """ + relation = self.model.get_relation( + relation_name=self.relationship_name, + relation_id=relation_id, + ) + if not relation: + raise RuntimeError( + f"No relation found with relation name {self.relationship_name} and " + f"relation ID {relation_id}" + ) + relation.data[self.model.unit]["certificate"] = certificate + relation.data[self.model.unit]["ca"] = ca + relation.data[self.model.unit]["chain"] = json.dumps(chain) + + def remove_certificate(self, relation_id: int) -> None: + """Remove a given certificate from relation data. + + Args: + relation_id (int): Relation ID + + Returns: + None + """ + relation = self.model.get_relation( + relation_name=self.relationship_name, + relation_id=relation_id, + ) + if not relation: + logger.warning( + f"Can't remove certificate - Non-existent relation '{self.relationship_name}'" + ) + return + unit_relation_data = relation.data[self.model.unit] + certificate_removed = False + if "certificate" in unit_relation_data: + relation.data[self.model.unit].pop("certificate") + certificate_removed = True + if "ca" in unit_relation_data: + relation.data[self.model.unit].pop("ca") + certificate_removed = True + if "chain" in unit_relation_data: + relation.data[self.model.unit].pop("chain") + certificate_removed = True + + if certificate_removed: + logger.warning("Certificate removed from relation data") + else: + logger.warning("Can't remove certificate - No certificate in relation data") + + +class CertificateTransferRequires(Object): + """TLS certificates requirer class to be instantiated by TLS certificates requirers.""" + + on = CertificateTransferRequirerCharmEvents() + + def __init__( + self, + charm: CharmBase, + relationship_name: str, + ): + """Generates/use private key and observes relation changed event. + + Args: + charm: Charm object + relationship_name: Juju relation name + """ + super().__init__(charm, relationship_name) + self.relationship_name = relationship_name + self.charm = charm + self.framework.observe( + charm.on[relationship_name].relation_changed, self._on_relation_changed + ) + self.framework.observe( + charm.on[relationship_name].relation_broken, self._on_relation_broken + ) + + @staticmethod + def _relation_data_is_valid(relation_data: dict) -> bool: + """Return whether relation data is valid based on json schema. + + Args: + relation_data: Relation data in dict format. + + Returns: + bool: Whether relation data is valid. + """ + try: + validate(instance=relation_data, schema=PROVIDER_JSON_SCHEMA) + return True + except exceptions.ValidationError: + return False + + def _on_relation_changed(self, event: RelationChangedEvent) -> None: + """Emit certificate available event. + + Args: + event: Juju event + + Returns: + None + """ + if not event.unit: + logger.info(f"No remote unit in relation: {self.relationship_name}") + return + remote_unit_relation_data = _load_relation_data(event.relation.data[event.unit]) + if not self._relation_data_is_valid(remote_unit_relation_data): + logger.warning( + f"Provider relation data did not pass JSON Schema validation: " + f"{event.relation.data[event.unit]}" + ) + return + self.on.certificate_available.emit( + certificate=remote_unit_relation_data.get("certificate"), + ca=remote_unit_relation_data.get("ca"), + chain=remote_unit_relation_data.get("chain"), + relation_id=event.relation.id, + ) + + def _on_relation_broken(self, event: RelationBrokenEvent) -> None: + """Handler triggered on relation broken event. + + Args: + event: Juju event + + Returns: + None + """ + self.on.certificate_removed.emit(relation_id=event.relation.id) diff --git a/ops-sunbeam/ops_sunbeam/charm.py b/ops-sunbeam/ops_sunbeam/charm.py index f7f38f2f..3ea089d2 100644 --- a/ops-sunbeam/ops_sunbeam/charm.py +++ b/ops-sunbeam/ops_sunbeam/charm.py @@ -540,6 +540,21 @@ class OSBaseOperatorCharmK8S(OSBaseOperatorCharm): super().__init__(framework) self.pebble_handlers = self.get_pebble_handlers() + def get_relation_handlers( + self, handlers: List[sunbeam_rhandlers.RelationHandler] = None + ) -> List[sunbeam_rhandlers.RelationHandler]: + """Relation handlers for the service.""" + handlers = handlers or [] + if self.can_add_handler("receive-ca-cert", handlers): + self.receive_ca_cert = ( + sunbeam_rhandlers.CertificateTransferRequiresHandler( + self, "receive-ca-cert", self.configure_charm + ) + ) + handlers.append(self.receive_ca_cert) + + return super().get_relation_handlers(handlers) + def get_pebble_handlers(self) -> List[sunbeam_chandlers.PebbleHandler]: """Pebble handlers for the operator.""" return [ diff --git a/ops-sunbeam/ops_sunbeam/relation_handlers.py b/ops-sunbeam/ops_sunbeam/relation_handlers.py index ae1808f5..7f63adad 100644 --- a/ops-sunbeam/ops_sunbeam/relation_handlers.py +++ b/ops-sunbeam/ops_sunbeam/relation_handlers.py @@ -40,6 +40,7 @@ from ops.model import ( ActiveStatus, BlockedStatus, SecretNotFoundError, + Unit, UnknownStatus, WaitingStatus, ) @@ -1781,3 +1782,84 @@ class UserIdentityResourceRequiresHandler(RelationHandler): def ready(self) -> bool: """Whether the relation is ready.""" return self.get_config_credentials() is not None + + +class CertificateTransferRequiresHandler(RelationHandler): + """Handle certificate transfer relation on the requires side.""" + + def __init__( + self, + charm: ops.charm.CharmBase, + relation_name: str, + callback_f: Callable, + mandatory: bool = False, + ): + """Create a new certificate-transfer requires handler. + + Create a new CertificateTransferRequiresHandler that receives the + certificates from the provider and updates certificates on all + the containers. + + :param charm: the Charm class the handler is for + :type charm: ops.charm.CharmBase + :param relation_name: the relation the handler is bound to + :type relation_name: str + :param callback_f: the function to call when the nodes are connected + :type callback_f: Callable + :param mandatory: If the relation is mandatory to proceed with + configuring charm + :type mandatory: bool + """ + super().__init__(charm, relation_name, callback_f, mandatory) + + def setup_event_handler(self) -> None: + """Configure event handlers for tls relation.""" + logger.debug("Setting up certificate transfer event handler") + + from charms.certificate_transfer_interface.v0.certificate_transfer import ( + CertificateTransferRequires, + ) + + recv_ca_cert = CertificateTransferRequires( + self.charm, "receive-ca-cert" + ) + self.framework.observe( + recv_ca_cert.on.certificate_available, + self._on_recv_ca_cert_available, + ) + self.framework.observe( + recv_ca_cert.on.certificate_removed, self._on_recv_ca_cert_removed + ) + return recv_ca_cert + + def _on_recv_ca_cert_available(self, event: ops.framework.EventBase): + self.callback_f(event) + + def _on_recv_ca_cert_removed(self, event: ops.framework.EventBase): + self.callback_f(event) + + @property + def ready(self) -> bool: + """Check if relation handler is ready.""" + return True + + def context(self) -> dict: + """Context containing ca cert data.""" + receive_ca_cert_relations = list( + self.model.relations[self.relation_name] + ) + if not receive_ca_cert_relations: + return {} + + ca_bundle = [] + for k, v in receive_ca_cert_relations[0].data.items(): + if isinstance(k, Unit) and k != self.model.unit: + ca = v.get("ca") + chain = json.loads(v.get("chain", "[]")) + if ca and ca not in ca_bundle: + ca_bundle.append(ca) + for chain_ in chain: + if chain_ not in ca_bundle: + ca_bundle.append(chain_) + + return {"ca_bundle": "\n".join(ca_bundle)} diff --git a/templates/ca-bundle.pem.j2 b/templates/ca-bundle.pem.j2 new file mode 100644 index 00000000..f8f62458 --- /dev/null +++ b/templates/ca-bundle.pem.j2 @@ -0,0 +1,3 @@ +{% if receive_ca_cert and receive_ca_cert.ca_bundle -%} +{{ receive_ca_cert.ca_bundle }} +{% endif %} diff --git a/templates/parts/identity-data b/templates/parts/identity-data index 706d9d13..5ca9f93c 100644 --- a/templates/parts/identity-data +++ b/templates/parts/identity-data @@ -1,9 +1,9 @@ -{% if identity_service.admin_auth_url -%} -auth_url = {{ identity_service.admin_auth_url }} -interface = admin -{% elif identity_service.internal_auth_url -%} +{% if identity_service.internal_auth_url -%} auth_url = {{ identity_service.internal_auth_url }} interface = internal +{% elif identity_service.admin_auth_url -%} +auth_url = {{ identity_service.admin_auth_url }} +interface = admin {% elif identity_service.internal_host -%} auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} interface = internal @@ -19,5 +19,8 @@ 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 }} +{% if receive_ca_cert and receive_ca_cert.ca_bundle -%} +cafile = /usr/local/share/ca-certificates/ca-bundle.pem +{% endif -%} service_token_roles = {{ identity_service.admin_role }} service_token_roles_required = True diff --git a/templates/parts/identity-data-id-creds b/templates/parts/identity-data-id-creds index e396178a..31d2d824 100644 --- a/templates/parts/identity-data-id-creds +++ b/templates/parts/identity-data-id-creds @@ -19,5 +19,8 @@ user_domain_name = {{ identity_credentials.user_domain_name }} project_name = {{ identity_credentials.project_name }} username = {{ identity_credentials.username }} password = {{ identity_credentials.password }} +{% if receive_ca_cert and receive_ca-cert.ca_bundle -%} +cafile = /usr/local/share/ca-certificates/ca-bundle.pem +{% endif -%} service_token_roles = {{ identity_credentials.admin_role }} service_token_roles_required = True diff --git a/tests/caas/smoke.yaml.j2 b/tests/caas/smoke.yaml.j2 index 93253264..85bbbde3 100644 --- a/tests/caas/smoke.yaml.j2 +++ b/tests/caas/smoke.yaml.j2 @@ -141,6 +141,8 @@ relations: - glance:amqp - - traefik:ingress - glance:ingress-public +- - keystone:send-ca-cert + - glance:receive-ca-cert - - mysql:database - heat:database @@ -152,6 +154,8 @@ relations: - heat:traefik-route-public - - rabbitmq:amqp - heat:amqp +- - keystone:send-ca-cert + - heat:receive-ca-cert - - mysql:database - octavia:database @@ -165,6 +169,8 @@ relations: - octavia:certificates - - octavia:ovsdb-cms - ovn-central:ovsdb-cms +- - keystone:send-ca-cert + - octavia:receive-ca-cert - - mysql:database - barbican:database @@ -178,6 +184,8 @@ relations: - barbican:ingress-public - - vault:vault-kv - barbican:vault-kv +- - keystone:send-ca-cert + - barbican:receive-ca-cert - - mysql:database - magnum:database @@ -189,3 +197,5 @@ relations: - magnum:identity-ops - - traefik:ingress - magnum:ingress-public +- - keystone:send-ca-cert + - magnum:receive-ca-cert diff --git a/tests/ceph/smoke.yaml.j2 b/tests/ceph/smoke.yaml.j2 index 5b4802f9..e78d7887 100644 --- a/tests/ceph/smoke.yaml.j2 +++ b/tests/ceph/smoke.yaml.j2 @@ -114,6 +114,8 @@ relations: - cinder:identity-service - - traefik:ingress - cinder:ingress-public +- - keystone:send-ca-cert + - cinder:receive-ca-cert - - cinder-ceph:database - mysql:database @@ -128,6 +130,8 @@ relations: - gnocchi:ingress-public - - keystone:identity-service - gnocchi:identity-service +- - keystone:send-ca-cert + - gnocchi:receive-ca-cert - - rabbitmq:amqp - ceilometer:amqp @@ -135,6 +139,8 @@ relations: - ceilometer:identity-credentials - - gnocchi:gnocchi-service - ceilometer:gnocchi-db +- - keystone:send-ca-cert + - ceilometer:receive-ca-cert - - mysql:database - aodh:database @@ -144,3 +150,5 @@ relations: - aodh:identity-service - - traefik:ingress - aodh:ingress-public +- - keystone:send-ca-cert + - aodh:receive-ca-cert diff --git a/tests/core/smoke.yaml.j2 b/tests/core/smoke.yaml.j2 index 884cb9a1..83886992 100644 --- a/tests/core/smoke.yaml.j2 +++ b/tests/core/smoke.yaml.j2 @@ -151,6 +151,8 @@ relations: - glance:amqp - - traefik:ingress - glance:ingress-public +- - keystone:send-ca-cert + - glance:receive-ca-cert - - mysql:database - nova:database @@ -164,6 +166,8 @@ relations: - nova:identity-service - - traefik:ingress - nova:ingress-public +- - keystone:send-ca-cert + - nova:receive-ca-cert - - mysql:database - placement:database @@ -171,6 +175,8 @@ relations: - placement:identity-service - - traefik:ingress - placement:ingress-public +- - keystone:send-ca-cert + - placement:receive-ca-cert - - mysql:database - neutron:database @@ -184,6 +190,8 @@ relations: - neutron:certificates - - neutron:ovsdb-cms - ovn-central:ovsdb-cms +- - keystone:send-ca-cert + - neutron:receive-ca-cert - - mysql:database - horizon:database @@ -191,3 +199,5 @@ relations: - horizon:identity-credentials - - traefik:ingress - horizon:ingress-public +- - keystone:send-ca-cert + - horizon:receive-ca-cert diff --git a/tests/misc/smoke.yaml.j2 b/tests/misc/smoke.yaml.j2 index 71c29199..829ed8ff 100644 --- a/tests/misc/smoke.yaml.j2 +++ b/tests/misc/smoke.yaml.j2 @@ -98,6 +98,8 @@ relations: - designate:ingress-public - - designate-bind:dns-backend - designate:dns-backend +- - keystone:send-ca-cert + - designate:receive-ca-cert - - keystone:domain-config - keystone-ldap:domain-config