Implement vault-kv relation
Change-Id: I8d6675c67711a397b0e0631d9f6724e691026c33
This commit is contained in:
parent
c11f1d4079
commit
f2ad0614c0
@ -19,6 +19,7 @@ messaging and keystone identity operators:
|
||||
juju relate mysql:database barbican:database
|
||||
juju relate rabbitmq:amqp barbican:amqp
|
||||
juju relate keystone:identity-service barbican:identity-service
|
||||
juju relate vault:vault-kv barbican:vault-kv
|
||||
|
||||
### Configuration
|
||||
|
||||
@ -43,13 +44,13 @@ barbican-k8s requires the following relations:
|
||||
`identity-service`: To register endpoints in Keystone
|
||||
`ingress-internal`: To expose service on underlying internal network
|
||||
`ingress-public`: To expose service on public network
|
||||
`vault-kv`: To store secrets in a Vault key-value store
|
||||
|
||||
## OCI Images
|
||||
|
||||
The charm by default uses following images:
|
||||
|
||||
`docker.io/kolla/ubuntu-binary-barbican-api:yoga`
|
||||
`docker.io/kolla/ubuntu-binary-barbican-worker:yoga`
|
||||
`ghcr.io/canonical/barbican-consolidated:2023.1`
|
||||
|
||||
## Contributing
|
||||
|
||||
|
528
charms/barbican-k8s/lib/charms/vault_k8s/v0/vault_kv.py
Normal file
528
charms/barbican-k8s/lib/charms/vault_k8s/v0/vault_kv.py
Normal file
@ -0,0 +1,528 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright 2023 Canonical Ltd.
|
||||
# See LICENSE file for licensing details.
|
||||
|
||||
"""Library for the vault-kv relation.
|
||||
|
||||
This library contains the Requires and Provides classes for handling the vault-kv
|
||||
interface.
|
||||
|
||||
## Getting Started
|
||||
From a charm directory, fetch the library using `charmcraft`:
|
||||
|
||||
```shell
|
||||
charmcraft fetch-lib charms.vault_k8s.v0.vault_kv
|
||||
```
|
||||
|
||||
### Requirer charm
|
||||
The requirer charm is the charm requiring a secret value store. In this example, the requirer charm
|
||||
is requiring a secret value store.
|
||||
|
||||
```python
|
||||
import secrets
|
||||
|
||||
from charms.vault_k8s.v0 import vault_kv
|
||||
from ops.charm import CharmBase, InstallEvent
|
||||
from ops.main import main
|
||||
from ops.model import ActiveStatus, BlockedStatus
|
||||
|
||||
NONCE_SECRET_LABEL = "nonce"
|
||||
|
||||
|
||||
class ExampleRequirerCharm(CharmBase):
|
||||
def __init__(self, *args):
|
||||
super().__init__(*args)
|
||||
self.interface = vault_kv.VaultKvRequires(
|
||||
self,
|
||||
"vault-kv",
|
||||
"my-suffix",
|
||||
)
|
||||
|
||||
self.framework.observe(self.on.install, self._on_install)
|
||||
self.framework.observe(self.interface.on.connected, self._on_connected)
|
||||
self.framework.observe(self.interface.on.ready, self._on_ready)
|
||||
self.framework.observe(self.interface.on.gone_away, self._on_gone_away)
|
||||
self.framework.observe(self.on.update_status, self._on_update_status)
|
||||
|
||||
def _on_install(self, event: InstallEvent):
|
||||
self.unit.add_secret(
|
||||
{"nonce": secrets.token_hex(16)},
|
||||
label=NONCE_SECRET_LABEL,
|
||||
description="Nonce for vault-kv relation",
|
||||
)
|
||||
self.unit.status = BlockedStatus("Waiting for vault-kv relation")
|
||||
|
||||
def _on_connected(self, event: vault_kv.VaultKvConnectedEvent):
|
||||
relation = self.model.get_relation(event.relation_name, event.relation_id)
|
||||
egress_subnet = str(self.model.get_binding(relation).network.interfaces[0].subnet)
|
||||
self.interface.request_credentials(relation, egress_subnet, self.get_nonce())
|
||||
|
||||
def _on_ready(self, event: vault_kv.VaultKvReadyEvent):
|
||||
relation = self.model.get_relation(event.relation_name, event.relation_id)
|
||||
if relation is None:
|
||||
return
|
||||
vault_url = self.interface.get_vault_url(relation)
|
||||
ca_certificate = self.interface.get_ca_certificate(relation)
|
||||
mount = self.interface.get_mount(relation)
|
||||
|
||||
unit_credentials = self.interface.get_unit_credentials(relation)
|
||||
# unit_credentials is a juju secret id
|
||||
secret = self.model.get_secret(id=unit_credentials)
|
||||
secret_content = secret.get_content()
|
||||
role_id = secret_content["role-id"]
|
||||
role_secret_id = secret_content["role-secret-id"]
|
||||
|
||||
self._configure(vault_url, ca_certificate, mount, role_id, role_secret_id)
|
||||
|
||||
self.unit.status = ActiveStatus()
|
||||
|
||||
def _on_gone_away(self, event: vault_kv.VaultKvGoneAwayEvent):
|
||||
self.unit.status = BlockedStatus("Waiting for vault-kv relation")
|
||||
|
||||
def _configure(
|
||||
self,
|
||||
vault_url: str,
|
||||
ca_certificate: str,
|
||||
mount: str,
|
||||
role_id: str,
|
||||
role_secret_id: str,
|
||||
):
|
||||
pass
|
||||
|
||||
def _on_update_status(self, event):
|
||||
# Check somewhere that egress subnet has not changed i.e. pod has not been rescheduled
|
||||
# Update status might not be the best place
|
||||
binding = self.model.get_binding("vault-kv")
|
||||
if binding is not None:
|
||||
egress_subnet = str(binding.network.interfaces[0].subnet)
|
||||
self.interface.request_credentials(event.relation, egress_subnet, self.get_nonce())
|
||||
|
||||
def get_nonce(self):
|
||||
secret = self.model.get_secret(label=NONCE_SECRET_LABEL)
|
||||
nonce = secret.get_content()["nonce"]
|
||||
return nonce
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(ExampleRequirerCharm)
|
||||
```
|
||||
|
||||
You can integrate both charms by running:
|
||||
|
||||
```bash
|
||||
juju integrate <vault provider charm> <vault requirer charm>
|
||||
```
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, Dict, Iterable, Mapping, Optional, Union
|
||||
|
||||
import ops
|
||||
from interface_tester.schema_base import DataBagSchema # type: ignore[import]
|
||||
from pydantic import BaseModel, Field, Json, ValidationError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# The unique Charmhub library identifier, never change it
|
||||
LIBID = "591d6d2fb6a54853b4bb53ef16ef603a"
|
||||
|
||||
# Increment this major API version when introducing breaking changes
|
||||
LIBAPI = 0
|
||||
|
||||
# Increment this PATCH version before using `charmcraft publish-lib` or reset
|
||||
# to 0 if you are raising the major API version
|
||||
LIBPATCH = 1
|
||||
|
||||
PYDEPS = ["pydantic", "pytest-interface-tester"]
|
||||
|
||||
|
||||
class VaultKvProviderSchema(BaseModel):
|
||||
"""Provider side of the vault-kv interface."""
|
||||
|
||||
vault_url: str = Field(description="The URL of the Vault server to connect to.")
|
||||
mount: str = Field(
|
||||
description=(
|
||||
"The KV mount available for the requirer application, "
|
||||
"respecting the pattern 'charm-<requirer app>-<user provided suffix>'."
|
||||
)
|
||||
)
|
||||
ca_certificate: str = Field(
|
||||
description="The CA certificate to use when validating the Vault server's certificate."
|
||||
)
|
||||
credentials: Json[Mapping[str, str]] = Field(
|
||||
description=(
|
||||
"Mapping of unit name and credentials for that unit."
|
||||
" Credentials are a juju secret containing a 'role-id' and a 'role-secret-id'."
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class AppVaultKvRequirerSchema(BaseModel):
|
||||
"""App schema of the requirer side of the vault-kv interface."""
|
||||
|
||||
mount_suffix: str = Field(
|
||||
description="Suffix to append to the mount name to get the KV mount."
|
||||
)
|
||||
|
||||
|
||||
class UnitVaultKvRequirerSchema(BaseModel):
|
||||
"""Unit schema of the requirer side of the vault-kv interface."""
|
||||
|
||||
egress_subnet: str = Field(description="Egress subnet to use, in CIDR notation.")
|
||||
nonce: str = Field(
|
||||
description="Uniquely identifying value for this unit. `secrets.token_hex(16)` is recommended."
|
||||
)
|
||||
|
||||
|
||||
class ProviderSchema(DataBagSchema):
|
||||
"""The schema for the provider side of this interface."""
|
||||
|
||||
app: VaultKvProviderSchema
|
||||
|
||||
|
||||
class RequirerSchema(DataBagSchema):
|
||||
"""The schema for the requirer side of this interface."""
|
||||
|
||||
app: AppVaultKvRequirerSchema
|
||||
unit: UnitVaultKvRequirerSchema
|
||||
|
||||
|
||||
def is_requirer_data_valid(app_data: dict, unit_data: dict) -> bool:
|
||||
"""Return whether the requirer data is valid."""
|
||||
try:
|
||||
RequirerSchema(
|
||||
app=AppVaultKvRequirerSchema(**app_data),
|
||||
unit=UnitVaultKvRequirerSchema(**unit_data),
|
||||
)
|
||||
return True
|
||||
except ValidationError as e:
|
||||
logger.debug("Invalid data: %s", e)
|
||||
return False
|
||||
|
||||
|
||||
def is_provider_data_valid(data: dict) -> bool:
|
||||
"""Return whether the provider data is valid."""
|
||||
try:
|
||||
ProviderSchema(app=VaultKvProviderSchema(**data))
|
||||
return True
|
||||
except ValidationError as e:
|
||||
logger.debug("Invalid data: %s", e)
|
||||
return False
|
||||
|
||||
|
||||
class NewVaultKvClientAttachedEvent(ops.EventBase):
|
||||
"""New vault kv client attached event."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
handle: ops.Handle,
|
||||
relation_id: int,
|
||||
relation_name: str,
|
||||
mount_suffix: str,
|
||||
):
|
||||
super().__init__(handle)
|
||||
self.relation_id = relation_id
|
||||
self.relation_name = relation_name
|
||||
self.mount_suffix = mount_suffix
|
||||
|
||||
def snapshot(self) -> dict:
|
||||
"""Return snapshot data that should be persisted."""
|
||||
return {
|
||||
"relation_id": self.relation_id,
|
||||
"relation_name": self.relation_name,
|
||||
"mount_suffix": self.mount_suffix,
|
||||
}
|
||||
|
||||
def restore(self, snapshot: Dict[str, Any]):
|
||||
"""Restore the value state from a given snapshot."""
|
||||
super().restore(snapshot)
|
||||
self.relation_id = snapshot["relation_id"]
|
||||
self.relation_name = snapshot["relation_name"]
|
||||
self.mount_suffix = snapshot["mount_suffix"]
|
||||
|
||||
|
||||
class VaultKvProviderEvents(ops.ObjectEvents):
|
||||
"""List of events that the Vault Kv provider charm can leverage."""
|
||||
|
||||
new_vault_kv_client_attached = ops.EventSource(NewVaultKvClientAttachedEvent)
|
||||
|
||||
|
||||
class VaultKvProvides(ops.Object):
|
||||
"""Class to be instanciated by the providing side of the relation."""
|
||||
|
||||
on = VaultKvProviderEvents()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
charm: ops.CharmBase,
|
||||
relation_name: str,
|
||||
) -> None:
|
||||
super().__init__(charm, relation_name)
|
||||
self.charm = charm
|
||||
self.relation_name = relation_name
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_changed,
|
||||
self._on_relation_changed,
|
||||
)
|
||||
|
||||
def _on_relation_changed(self, event: ops.RelationChangedEvent):
|
||||
"""Handle client changed relation.
|
||||
|
||||
This handler will emit a new_vault_kv_client_attached event if at least one unit data is
|
||||
valid.
|
||||
"""
|
||||
if event.app is None:
|
||||
logger.debug("No remote application yet")
|
||||
return
|
||||
|
||||
app_data = dict(event.relation.data[event.app])
|
||||
|
||||
any_valid = False
|
||||
for unit in event.relation.units:
|
||||
if not is_requirer_data_valid(app_data, dict(event.relation.data[unit])):
|
||||
logger.debug("Invalid data from unit %r", unit.name)
|
||||
continue
|
||||
any_valid = True
|
||||
|
||||
if any_valid:
|
||||
self.on.new_vault_kv_client_attached.emit(
|
||||
event.relation.id,
|
||||
event.relation.name,
|
||||
event.relation.data[event.app]["mount_suffix"],
|
||||
)
|
||||
|
||||
def set_vault_url(self, relation: ops.Relation, vault_url: str):
|
||||
"""Set the vault_url on the relation."""
|
||||
if not self.charm.unit.is_leader():
|
||||
return
|
||||
|
||||
relation.data[self.charm.app]["vault_url"] = vault_url
|
||||
|
||||
def set_ca_certificate(self, relation: ops.Relation, ca_certificate: str):
|
||||
"""Set the ca_certificate on the relation."""
|
||||
if not self.charm.unit.is_leader():
|
||||
return
|
||||
|
||||
relation.data[self.charm.app]["ca_certificate"] = ca_certificate
|
||||
|
||||
def set_mount(self, relation: ops.Relation, mount: str):
|
||||
"""Set the mount on the relation."""
|
||||
if not self.charm.unit.is_leader():
|
||||
return
|
||||
|
||||
relation.data[self.charm.app]["mount"] = mount
|
||||
|
||||
def set_unit_credentials(self, relation: ops.Relation, nonce: str, secret: ops.Secret):
|
||||
"""Set the unit credentials on the relation."""
|
||||
if not self.charm.unit.is_leader():
|
||||
return
|
||||
|
||||
credentials = self.get_credentials(relation)
|
||||
if secret.id is None:
|
||||
logger.debug(
|
||||
"Secret id is None, not updating the relation '%s:%d' for nonce %r",
|
||||
relation.name,
|
||||
relation.id,
|
||||
nonce,
|
||||
)
|
||||
return
|
||||
credentials[nonce] = secret.id
|
||||
relation.data[self.charm.app]["credentials"] = json.dumps(credentials, sort_keys=True)
|
||||
|
||||
def remove_unit_credentials(self, relation: ops.Relation, nonce: Union[str, Iterable[str]]):
|
||||
"""Remove nonce(s) from the relation."""
|
||||
if not self.charm.unit.is_leader():
|
||||
return
|
||||
|
||||
if isinstance(nonce, str):
|
||||
nonce = [nonce]
|
||||
|
||||
credentials = self.get_credentials(relation)
|
||||
|
||||
for n in nonce:
|
||||
credentials.pop(n, None)
|
||||
|
||||
relation.data[self.charm.app]["credentials"] = json.dumps(credentials, sort_keys=True)
|
||||
|
||||
def get_credentials(self, relation: ops.Relation) -> dict:
|
||||
"""Get the unit credentials from the relation."""
|
||||
return json.loads(relation.data[self.charm.app].get("credentials", "{}"))
|
||||
|
||||
|
||||
class VaultKvConnectedEvent(ops.EventBase):
|
||||
"""VaultKvConnectedEvent Event."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
handle: ops.Handle,
|
||||
relation_id: int,
|
||||
relation_name: str,
|
||||
):
|
||||
super().__init__(handle)
|
||||
self.relation_id = relation_id
|
||||
self.relation_name = relation_name
|
||||
|
||||
def snapshot(self) -> dict:
|
||||
"""Return snapshot data that should be persisted."""
|
||||
return {
|
||||
"relation_id": self.relation_id,
|
||||
"relation_name": self.relation_name,
|
||||
}
|
||||
|
||||
def restore(self, snapshot: Dict[str, Any]):
|
||||
"""Restore the value state from a given snapshot."""
|
||||
super().restore(snapshot)
|
||||
self.relation_id = snapshot["relation_id"]
|
||||
self.relation_name = snapshot["relation_name"]
|
||||
|
||||
|
||||
class VaultKvReadyEvent(ops.EventBase):
|
||||
"""VaultKvReadyEvent Event."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
handle: ops.Handle,
|
||||
relation_id: int,
|
||||
relation_name: str,
|
||||
):
|
||||
super().__init__(handle)
|
||||
self.relation_id = relation_id
|
||||
self.relation_name = relation_name
|
||||
|
||||
def snapshot(self) -> dict:
|
||||
"""Return snapshot data that should be persisted."""
|
||||
return {
|
||||
"relation_id": self.relation_id,
|
||||
"relation_name": self.relation_name,
|
||||
}
|
||||
|
||||
def restore(self, snapshot: Dict[str, Any]):
|
||||
"""Restore the value state from a given snapshot."""
|
||||
super().restore(snapshot)
|
||||
self.relation_id = snapshot["relation_id"]
|
||||
self.relation_name = snapshot["relation_name"]
|
||||
|
||||
|
||||
class VaultKvGoneAwayEvent(ops.EventBase):
|
||||
"""VaultKvGoneAwayEvent Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class VaultKvRequireEvents(ops.ObjectEvents):
|
||||
"""List of events that the Vault Kv requirer charm can leverage."""
|
||||
|
||||
connected = ops.EventSource(VaultKvConnectedEvent)
|
||||
ready = ops.EventSource(VaultKvReadyEvent)
|
||||
gone_away = ops.EventSource(VaultKvGoneAwayEvent)
|
||||
|
||||
|
||||
class VaultKvRequires(ops.Object):
|
||||
"""Class to be instanciated by the requiring side of the relation."""
|
||||
|
||||
on = VaultKvRequireEvents()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
charm: ops.CharmBase,
|
||||
relation_name: str,
|
||||
mount_suffix: str,
|
||||
) -> None:
|
||||
super().__init__(charm, relation_name)
|
||||
self.charm = charm
|
||||
self.relation_name = relation_name
|
||||
self.mount_suffix = mount_suffix
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_joined,
|
||||
self._on_vault_kv_relation_joined,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_changed,
|
||||
self._on_vault_kv_relation_changed,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_broken,
|
||||
self._on_vault_kv_relation_broken,
|
||||
)
|
||||
|
||||
def _set_unit_nonce(self, relation: ops.Relation, nonce: str):
|
||||
"""Set the nonce on the relation."""
|
||||
relation.data[self.charm.unit]["nonce"] = nonce
|
||||
|
||||
def _set_unit_egress_subnet(self, relation: ops.Relation, egress_subnet: str):
|
||||
"""Set the egress_subnet on the relation."""
|
||||
relation.data[self.charm.unit]["egress_subnet"] = egress_subnet
|
||||
|
||||
def _on_vault_kv_relation_joined(self, event: ops.RelationJoinedEvent):
|
||||
"""Handle relation joined.
|
||||
|
||||
Set the secret backend in the application databag if we are the leader.
|
||||
Always update the egress_subnet in the unit databag.
|
||||
"""
|
||||
if self.charm.unit.is_leader():
|
||||
event.relation.data[self.charm.app]["mount_suffix"] = self.mount_suffix
|
||||
self.on.connected.emit(
|
||||
event.relation.id,
|
||||
event.relation.name,
|
||||
)
|
||||
|
||||
def _on_vault_kv_relation_changed(self, event: ops.RelationChangedEvent):
|
||||
"""Handle relation changed."""
|
||||
if event.app is None:
|
||||
logger.debug("No remote application yet")
|
||||
return
|
||||
|
||||
if (
|
||||
is_provider_data_valid(dict(event.relation.data[event.app]))
|
||||
and self.get_unit_credentials(event.relation) is not None
|
||||
):
|
||||
self.on.ready.emit(
|
||||
event.relation.id,
|
||||
event.relation.name,
|
||||
)
|
||||
|
||||
def _on_vault_kv_relation_broken(self, event: ops.RelationBrokenEvent):
|
||||
"""Handle relation broken."""
|
||||
self.on.gone_away.emit()
|
||||
|
||||
def request_credentials(self, relation: ops.Relation, egress_subnet: str, nonce: str) -> None:
|
||||
"""Request credentials from the vault-kv relation.
|
||||
|
||||
Generated secret ids are tied to the unit egress_subnet, so if the egress_subnet
|
||||
changes a new secret id must be generated.
|
||||
|
||||
A change in egress_subnet can happen when the pod is rescheduled to a different
|
||||
node by the underlying substrate without a change from Juju.
|
||||
"""
|
||||
self._set_unit_egress_subnet(relation, egress_subnet)
|
||||
self._set_unit_nonce(relation, nonce)
|
||||
|
||||
def get_vault_url(self, relation: ops.Relation) -> Optional[str]:
|
||||
"""Return the vault_url from the relation."""
|
||||
if relation.app is None:
|
||||
return None
|
||||
return relation.data[relation.app].get("vault_url")
|
||||
|
||||
def get_ca_certificate(self, relation: ops.Relation) -> Optional[str]:
|
||||
"""Return the ca_certificate from the relation."""
|
||||
if relation.app is None:
|
||||
return None
|
||||
return relation.data[relation.app].get("ca_certificate")
|
||||
|
||||
def get_mount(self, relation: ops.Relation) -> Optional[str]:
|
||||
"""Return the mount from the relation."""
|
||||
if relation.app is None:
|
||||
return None
|
||||
return relation.data[relation.app].get("mount")
|
||||
|
||||
def get_unit_credentials(self, relation: ops.Relation) -> Optional[str]:
|
||||
"""Return the unit credentials from the relation.
|
||||
|
||||
Unit credentials are stored in the relation data as a Juju secret id.
|
||||
"""
|
||||
nonce = relation.data[self.charm.unit].get("nonce")
|
||||
if nonce is None or relation.app is None:
|
||||
return None
|
||||
return json.loads(relation.data[relation.app].get("credentials", "{}")).get(nonce)
|
@ -13,6 +13,14 @@ bases:
|
||||
- name: ubuntu
|
||||
channel: 22.04/stable
|
||||
|
||||
assumes:
|
||||
- k8s-api
|
||||
- juju >= 3.1
|
||||
tags:
|
||||
- openstack
|
||||
- secrets
|
||||
- misc
|
||||
|
||||
requires:
|
||||
ingress-internal:
|
||||
interface: ingress
|
||||
@ -28,6 +36,9 @@ requires:
|
||||
interface: keystone
|
||||
amqp:
|
||||
interface: rabbitmq
|
||||
vault-kv:
|
||||
interface: vault-kv
|
||||
limit: 1
|
||||
|
||||
peers:
|
||||
peers:
|
||||
@ -43,8 +54,8 @@ resources:
|
||||
barbican-api-image:
|
||||
type: oci-image
|
||||
description: OCI image for OpenStack Barbican API
|
||||
upstream-source: ghcr.io/openstack-snaps/barbican-consolidated:2023.1
|
||||
upstream-source: ghcr.io/canonical/barbican-consolidated:2023.1
|
||||
barbican-worker-image:
|
||||
type: oci-image
|
||||
description: OCI image for OpenStack Barbican worker
|
||||
upstream-source: ghcr.io/openstack-snaps/barbican-consolidated:2023.1
|
||||
upstream-source: ghcr.io/canonical/barbican-consolidated:2023.1
|
||||
|
@ -12,4 +12,5 @@ lightkube
|
||||
lightkube-models
|
||||
ops
|
||||
pwgen
|
||||
pytest-interface-tester
|
||||
git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
|
||||
|
@ -18,14 +18,22 @@
|
||||
This charm provide Barbican services as part of an OpenStack deployment
|
||||
"""
|
||||
import logging
|
||||
import secrets
|
||||
from typing import (
|
||||
List,
|
||||
Optional,
|
||||
)
|
||||
|
||||
import ops
|
||||
import ops.framework
|
||||
import ops_sunbeam.charm as sunbeam_charm
|
||||
import ops_sunbeam.config_contexts as sunbeam_ctxts
|
||||
import ops_sunbeam.container_handlers as sunbeam_chandlers
|
||||
import ops_sunbeam.core as sunbeam_core
|
||||
import ops_sunbeam.relation_handlers as sunbeam_rhandlers
|
||||
from charms.vault_k8s.v0 import (
|
||||
vault_kv,
|
||||
)
|
||||
from ops import (
|
||||
framework,
|
||||
model,
|
||||
@ -39,6 +47,14 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
BARBICAN_API_CONTAINER = "barbican-api"
|
||||
BARBICAN_WORKER_CONTAINER = "barbican-worker"
|
||||
VAULT_KV_RELATION = "vault-kv"
|
||||
NONCE_SECRET_LABEL = "nonce"
|
||||
|
||||
|
||||
class NoRelationError(Exception):
|
||||
"""No relation found."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class WSGIBarbicanAdminConfigContext(sunbeam_ctxts.ConfigContext):
|
||||
@ -58,6 +74,117 @@ class WSGIBarbicanAdminConfigContext(sunbeam_ctxts.ConfigContext):
|
||||
}
|
||||
|
||||
|
||||
class VaultKvRequiresHandler(sunbeam_rhandlers.RelationHandler):
|
||||
"""Handler for vault-kv relation."""
|
||||
|
||||
charm: "BarbicanVaultOperatorCharm"
|
||||
interface: vault_kv.VaultKvRequires
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
charm: ops.CharmBase,
|
||||
relation_name: str,
|
||||
callback_f,
|
||||
mount_suffix: str,
|
||||
mandatory: bool = False,
|
||||
):
|
||||
self.mount_suffix = mount_suffix
|
||||
super().__init__(charm, relation_name, callback_f, mandatory)
|
||||
|
||||
def setup_event_handler(self) -> ops.Object:
|
||||
"""Configure event handlers for a vault-kv relation."""
|
||||
logger.debug("Setting up vault-kv event handler")
|
||||
interface = vault_kv.VaultKvRequires(
|
||||
self.charm,
|
||||
self.relation_name,
|
||||
self.mount_suffix,
|
||||
)
|
||||
|
||||
self.framework.observe(interface.on.connected, self._on_connected)
|
||||
self.framework.observe(interface.on.ready, self._on_ready)
|
||||
self.framework.observe(interface.on.gone_away, self._on_gone_away)
|
||||
try:
|
||||
self.request_credentials(interface, self._relation)
|
||||
except NoRelationError:
|
||||
pass
|
||||
return interface
|
||||
|
||||
@property
|
||||
def _relation(self) -> ops.Relation:
|
||||
relation = self.model.get_relation(VAULT_KV_RELATION)
|
||||
if relation is None:
|
||||
raise NoRelationError("Vault-kv relation not found")
|
||||
return relation
|
||||
|
||||
def _on_connected(self, event: vault_kv.VaultKvConnectedEvent):
|
||||
"""Handle on connected event."""
|
||||
relation = self.model.get_relation(
|
||||
event.relation_name, event.relation_id
|
||||
)
|
||||
if relation is None:
|
||||
raise RuntimeError(
|
||||
"Vault-kv relation not found during a connected event"
|
||||
)
|
||||
self.request_credentials(self.interface, relation)
|
||||
|
||||
def _on_ready(self, event: vault_kv.VaultKvReadyEvent):
|
||||
"""Handle client ready relation."""
|
||||
self.callback_f(event)
|
||||
|
||||
def _on_gone_away(self, event: vault_kv.VaultKvGoneAwayEvent):
|
||||
"""Handle client gone away relation."""
|
||||
self.callback_f(event)
|
||||
|
||||
def request_credentials(
|
||||
self, interface: vault_kv.VaultKvRequires, relation: ops.Relation
|
||||
):
|
||||
"""Request credentials from vault-kv relation."""
|
||||
nonce = self.charm.get_nonce()
|
||||
if nonce is None:
|
||||
return
|
||||
binding = self.model.get_binding(relation)
|
||||
if binding is None:
|
||||
logger.debug("No binding found for vault-kv relation")
|
||||
return
|
||||
egress_subnet = str(binding.network.interfaces[0].subnet)
|
||||
interface.request_credentials(relation, egress_subnet, nonce)
|
||||
|
||||
@property
|
||||
def ready(self) -> bool:
|
||||
"""Whether the handler is ready for use."""
|
||||
relation = self.model.get_relation(VAULT_KV_RELATION)
|
||||
if relation is None:
|
||||
return False
|
||||
return all(
|
||||
(
|
||||
self.interface.get_unit_credentials(relation),
|
||||
self.interface.get_vault_url(relation),
|
||||
self.interface.get_mount(relation),
|
||||
)
|
||||
)
|
||||
|
||||
def context(self) -> dict:
|
||||
"""Context containing relation data."""
|
||||
vault_kv_relation = self._relation
|
||||
unit_credentials = self.interface.get_unit_credentials(
|
||||
vault_kv_relation
|
||||
)
|
||||
if not unit_credentials:
|
||||
return {}
|
||||
secret = self.model.get_secret(id=unit_credentials)
|
||||
secret_content = secret.get_content()
|
||||
return {
|
||||
"kv_mountpoint": self.interface.get_mount(vault_kv_relation),
|
||||
"vault_url": self.interface.get_vault_url(vault_kv_relation),
|
||||
"approle_role_id": secret_content["role-id"],
|
||||
"approle_secret_id": secret_content["role-secret-id"],
|
||||
"ca_crt_file": self.charm.ca_crt_file,
|
||||
"ca_certificate": self.interface.get_ca_certificate(
|
||||
vault_kv_relation
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class BarbicanWorkerPebbleHandler(sunbeam_chandlers.ServicePebbleHandler):
|
||||
"""Pebble handler for Barbican worker."""
|
||||
|
||||
@ -221,5 +348,71 @@ class BarbicanOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
||||
return super().healthcheck_http_url + "?build"
|
||||
|
||||
|
||||
class BarbicanVaultOperatorCharm(BarbicanOperatorCharm):
|
||||
"""Vault specialized Barbican Operator Charm."""
|
||||
|
||||
mandatory_relations = BarbicanOperatorCharm.mandatory_relations.union(
|
||||
{VAULT_KV_RELATION}
|
||||
)
|
||||
|
||||
def __init__(self, *args):
|
||||
super().__init__(*args)
|
||||
self.framework.observe(self.on.install, self._on_install)
|
||||
|
||||
def _on_install(self, event: ops.framework.EventBase) -> None:
|
||||
"""Handle install event."""
|
||||
self.unit.add_secret(
|
||||
{"nonce": secrets.token_hex(16)},
|
||||
label=NONCE_SECRET_LABEL,
|
||||
description="nonce for vault-kv relation",
|
||||
)
|
||||
|
||||
def get_relation_handlers(
|
||||
self,
|
||||
handlers: Optional[List[sunbeam_rhandlers.RelationHandler]] = None,
|
||||
) -> List[sunbeam_rhandlers.RelationHandler]:
|
||||
"""Relation handlers for the service."""
|
||||
handlers = super().get_relation_handlers(handlers)
|
||||
if self.can_add_handler(VAULT_KV_RELATION, handlers):
|
||||
self.vault_kv = VaultKvRequiresHandler(
|
||||
self,
|
||||
VAULT_KV_RELATION,
|
||||
self.configure_charm,
|
||||
self.mount_suffix,
|
||||
VAULT_KV_RELATION in self.mandatory_relations,
|
||||
)
|
||||
handlers.append(self.vault_kv)
|
||||
return handlers
|
||||
|
||||
@property
|
||||
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"
|
||||
)
|
||||
)
|
||||
return _cconfigs
|
||||
|
||||
@property
|
||||
def mount_suffix(self):
|
||||
"""Secret backend for vault."""
|
||||
return "secrets"
|
||||
|
||||
@property
|
||||
def ca_crt_file(self):
|
||||
"""Vault CA certificate file location."""
|
||||
return "/etc/barbican/vault_ca.crt"
|
||||
|
||||
def get_nonce(self) -> Optional[str]:
|
||||
"""Return nonce stored in secret."""
|
||||
try:
|
||||
secret = self.model.get_secret(label=NONCE_SECRET_LABEL)
|
||||
return secret.get_content()["nonce"]
|
||||
except ops.SecretNotFoundError:
|
||||
return None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(BarbicanOperatorCharm)
|
||||
main(BarbicanVaultOperatorCharm)
|
||||
|
@ -14,7 +14,11 @@ db_auto_create = false
|
||||
{% include "parts/section-service-user" %}
|
||||
|
||||
[secretstore]
|
||||
{% if vault_kv and vault_kv.approle_role_id -%}
|
||||
enabled_secretstore_plugins = vault_plugin
|
||||
{% else -%}
|
||||
enabled_secretstore_plugins = store_crypto
|
||||
{% endif -%}
|
||||
|
||||
[crypto]
|
||||
enabled_crypto_plugins = simple_crypto
|
||||
@ -22,3 +26,13 @@ enabled_crypto_plugins = simple_crypto
|
||||
[simple_crypto_plugin]
|
||||
# the kek should be a 32-byte value which is base64 encoded
|
||||
kek = 'YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoxMjM0NTY='
|
||||
|
||||
{% if vault_kv and vault_kv.approle_secret_id -%}
|
||||
[vault_plugin]
|
||||
approle_role_id = {{ vault_kv.approle_role_id }}
|
||||
approle_secret_id = {{ vault_kv.approle_secret_id }}
|
||||
kv_mountpoint = {{ vault_kv.kv_mountpoint }}
|
||||
vault_url = {{ vault_kv.vault_url }}
|
||||
use_ssl = True
|
||||
ssl_ca_crt_file = {{ vault_kv.ca_crt_file }}
|
||||
{% endif -%}
|
||||
|
3
charms/barbican-k8s/src/templates/vault_ca.crt.j2
Normal file
3
charms/barbican-k8s/src/templates/vault_ca.crt.j2
Normal file
@ -0,0 +1,3 @@
|
||||
{% if vault_kv is defined and vault_kv.ca_certificate -%}
|
||||
{{ vault_kv.ca_certificate }}
|
||||
{% endif -%}
|
@ -34,13 +34,19 @@ applications:
|
||||
fernet-keys: 5M
|
||||
credential-keys: 5M
|
||||
|
||||
vault:
|
||||
charm: ch:vault-k8s
|
||||
channel: latest/edge
|
||||
scale: 1
|
||||
trust: false
|
||||
|
||||
barbican:
|
||||
charm: ../../barbican-k8s.charm
|
||||
scale: 1
|
||||
trust: false
|
||||
resources:
|
||||
barbican-api-image: ghcr.io/openstack-snaps/barbican-consolidated:2023.1
|
||||
barbican-worker-image: ghcr.io/openstack-snaps/barbican-consolidated:2023.1
|
||||
barbican-api-image: ghcr.io/canonical/barbican-consolidated:2023.1
|
||||
barbican-worker-image: ghcr.io/canonical/barbican-consolidated:2023.1
|
||||
|
||||
relations:
|
||||
- - traefik:ingress
|
||||
@ -61,3 +67,5 @@ relations:
|
||||
- barbican:ingress-internal
|
||||
- - traefik:ingress
|
||||
- barbican:ingress-public
|
||||
- - vault:vault-kv
|
||||
- barbican:vault-kv
|
||||
|
@ -21,9 +21,6 @@ target_deploy_status:
|
||||
traefik:
|
||||
workload-status: active
|
||||
workload-status-message-regex: '^$'
|
||||
traefik-public:
|
||||
workload-status: active
|
||||
workload-status-message-regex: '^$'
|
||||
rabbitmq:
|
||||
workload-status: active
|
||||
workload-status-message-regex: '^$'
|
||||
@ -35,4 +32,7 @@ target_deploy_status:
|
||||
workload-status-message-regex: '^.*$'
|
||||
barbican:
|
||||
workload-status: active
|
||||
workload-status-message-regex: '^.*$'
|
||||
workload-status-message-regex: '^$'
|
||||
vault:
|
||||
workload-status: active
|
||||
workload-status-message-regex: '^$'
|
||||
|
Loading…
x
Reference in New Issue
Block a user