Make keystone as certificate transfer provider

Add functionality to keystone to act as a
certificate transfer provider.
Add actions to add, remove, list CA certs
to keystone.
Add Certificate Transfer requires handler
in ops_sunbeam. Update keystone_auth section
cafile option if certificate is available
in receive-ca-cert relation.
Update metadata.yaml for keystone and rest of
k8s charms.

Change-Id: I9c800e8f8a0c9197b195331be7b445bafe794780
This commit is contained in:
Hemanth Nakkina 2024-02-08 09:53:00 +05:30
parent dd6000bb51
commit bd057784d5
No known key found for this signature in database
GPG Key ID: 2E4970F7B143168E
48 changed files with 1251 additions and 97 deletions

View File

@ -72,6 +72,9 @@ requires:
limit: 1
amqp:
interface: rabbitmq
receive-ca-cert:
interface: certificate_transfer
optional: true
provides:
aodh:

View File

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

View File

@ -42,6 +42,9 @@ requires:
vault-kv:
interface: vault-kv
limit: 1
receive-ca-cert:
interface: certificate_transfer
optional: true
peers:
peers:

View File

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

View File

@ -49,6 +49,9 @@ requires:
limit: 1
gnocchi-db:
interface: gnocchi
receive-ca-cert:
interface: certificate_transfer
optional: true
peers:
peers:

View File

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

View File

@ -57,6 +57,9 @@ requires:
image-service:
interface: glance
optional: true
receive-ca-cert:
interface: certificate_transfer
optional: true
peers:
peers:

View File

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

View File

@ -45,6 +45,9 @@ requires:
dns-backend:
interface: bind-rndc
limit: 1
receive-ca-cert:
interface: certificate_transfer
optional: true
peers:
peers:

View File

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

View File

@ -67,6 +67,9 @@ requires:
ceph:
interface: ceph-client
optional: true
receive-ca-cert:
interface: certificate_transfer
optional: true
provides:
image-service:

View File

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

View File

@ -51,6 +51,9 @@ requires:
limit: 1
ceph:
interface: ceph-client
receive-ca-cert:
interface: certificate_transfer
optional: true
provides:
gnocchi-service:

View File

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

View File

@ -54,6 +54,9 @@ requires:
interface: rabbitmq
identity-ops:
interface: keystone-resources
receive-ca-cert:
interface: certificate_transfer
optional: true
peers:
peers:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -29,6 +29,8 @@ provides:
interface: keystone-credentials
identity-ops:
interface: keystone-resources
send-ca-cert:
interface: certificate_transfer
requires:
database:

View File

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

View File

@ -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-----
<cert 1>
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
<cert 2>
-----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

View File

@ -51,6 +51,9 @@ requires:
limit: 1
amqp:
interface: rabbitmq
receive-ca-cert:
interface: certificate_transfer
optional: true
peers:
peers:

View File

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

View File

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

View File

@ -57,6 +57,9 @@ requires:
certificates:
interface: tls-certificates
optional: true
receive-ca-cert:
interface: certificate_transfer
optional: true
peers:
peers:

View File

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

View File

@ -80,6 +80,9 @@ requires:
interface: neutron-api
placement:
interface: placement
receive-ca-cert:
interface: certificate_transfer
optional: true
provides:
cloud-controller:

View File

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

View File

@ -76,6 +76,9 @@ requires:
identity-ops:
interface: keystone-resources
optional: true
receive-ca-cert:
interface: certificate_transfer
optional: true
peers:
peers:

View File

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

View File

@ -28,6 +28,9 @@ resources:
requires:
identity-ops:
interface: keystone-resources
receive-ca-cert:
interface: certificate_transfer
optional: true
provides:
metrics-endpoint:

View File

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

View File

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

View File

@ -40,6 +40,10 @@ requires:
interface: ingress
optional: true
limit: 1
receive-ca-cert:
interface: certificate_transfer
optional: true
provides:
placement:
interface: placement

View File

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

195
common.sh
View File

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

View File

@ -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 <certificate_transfer provider charm> <certificate_transfer requirer charm>
```
"""
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)

View File

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

View File

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

View File

@ -0,0 +1,3 @@
{% if receive_ca_cert and receive_ca_cert.ca_bundle -%}
{{ receive_ca_cert.ca_bundle }}
{% endif %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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