Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 42 additions & 18 deletions interfaces/connect_client/interface/v0/schema.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,32 @@
"""This file defines the schemas for the provider and requirer sides of `connect_client` charm relation interface.
"""This file defines the schemas for the provider and requirer sides of `connect_client` charm relation interface."""

It exposes two interfaces.schema_base.DataBagSchema subclasses called:
- ProviderSchema
- RequirerSchema
"""
import json
from typing import Any

from interface_tester.schema_base import DataBagSchema
from pydantic import BaseModel, Field
from pydantic import BaseModel, ConfigDict, Field, field_serializer, field_validator

PLUGIN_URL_NOT_REQUIRED = "NOT-REQUIRED"


class ConnectProviderData(BaseModel):
class _BareStringDatabag(BaseModel):
"""Base class for databag models that don't strictly JSON encode all entries."""

@staticmethod
def __juju_decoder__(value: str) -> str:
"""Pass Juju's string through unmodified to be decoded by individual field validators."""
return value

@staticmethod
def __juju_encoder__(value: str | None) -> str:
"""Convert `None` to "", erasing the value; Ops will error on a non-string."""
return "" if value is None else value


class ProviderAppData(_BareStringDatabag):
"""The provider's application databag."""

model_config = ConfigDict(strict=True, populate_by_name=True)

endpoints: str = Field(
description="A comma-separated list of Kafka Connect REST endpoint(s), including the protocol (either `http` or `https`)",
examples=["http://10.1.1.100:8083,http://10.1.1.101:8083,http://10.1.1.102:8083"],
Expand All @@ -32,7 +47,11 @@ class ConnectProviderData(BaseModel):
)


class ConnectRequirerData(BaseModel):
class RequirerAppData(_BareStringDatabag):
"""The requirer's application databag."""

model_config = ConfigDict(strict=True, populate_by_name=True)

plugin_url: str = Field(
description=f'URL at which the plugins required by this client are served as a single Tarball. If not required, the requirer should place the sentinel value "{PLUGIN_URL_NOT_REQUIRED}"',
alias="plugin-url",
Expand All @@ -41,19 +60,24 @@ class ConnectRequirerData(BaseModel):
)
requested_secrets: list[str] = Field(
alias="requested-secrets",
description="Any provider field which should be transfered as Juju Secret",
description="Any provider field which should be transfered as Juju Secret. A JSON array on the wire.",
examples=[["username", "password", "tls-ca"]],
title="Requested secrets",
)

@field_validator("requested_secrets", mode="before")
@classmethod
def _load_json(cls, value: Any) -> Any:
if not isinstance(value, str):
return value # __init__ argument was already deserialized.
return json.loads(value)

class ProviderSchema(DataBagSchema):
"""The schema for the provider side of the `connect_client` interface."""

app: ConnectProviderData

@field_serializer("requested_secrets")
def _dump_json(self, value: object) -> str | None:
if value is None:
return None
return json.dumps(value)

class RequirerSchema(DataBagSchema):
"""The schema for the requirer side of the `connect_client` interface."""

app: ConnectRequirerData
ProviderUnitData = None
RequirerUnitData = None
74 changes: 39 additions & 35 deletions interfaces/kafka_client/interface/v0/schema.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
"""This file defines the schemas for the provider and requirer sides of the kafka_client interface.

It must expose two interfaces.schema_base.DataBagSchema subclasses called:
- ProviderSchema
- RequirerSchema
"""
"""This file defines the schemas for the provider and requirer sides of the kafka_client interface."""

from enum import Enum

from interface_tester.schema_base import DataBagSchema
from pydantic import BaseModel, Field, field_validator
from pydantic import BaseModel, ConfigDict, Field, field_validator


class ExtraUserRole(str, Enum):
Expand All @@ -17,87 +11,105 @@ class ExtraUserRole(str, Enum):
producer = "producer"


class KafkaProviderData(BaseModel):
class _BareStringDatabag(BaseModel):
"""Base class for databag models that don't strictly JSON encode all entries."""

@staticmethod
def __juju_decoder__(value: str) -> str:
"""Pass Juju's string through unmodified to be decoded by individual field validators."""
return value

@staticmethod
def __juju_encoder__(value: str | None) -> str:
"""Convert `None` to "", erasing the value; Ops will error on a non-string."""
return "" if value is None else value


class ProviderAppData(_BareStringDatabag):
"""The databag for the provider side of this interface."""

model_config = ConfigDict(strict=True, populate_by_name=True)

topic: str = Field(
description="The topic that has been made available to the relation user. Name defined in the Requirer's topic field",
description="The topic that has been made available to the relation user. Name defined in the Requirer's topic field. A bare string on the wire",
examples=["topic-1", "appname-*"],
title="Topic name",
)

username: str = Field(
description="Username for connecting to the Kafka cluster",
description="Username for connecting to the Kafka cluster. A bare string on the wire, but usually delivered in a Juju secret instead, in which case the key is absent from the databag",
examples=["relation-14"],
title="Kafka SASL/SCRAM username",
)

password: str = Field(
description="Password for connecting to the Kafka cluster",
description="Password for connecting to the Kafka cluster. A bare string on the wire, but usually delivered in a Juju secret instead, in which case the key is absent from the databag",
examples=["alphanum-32byte-random"],
title="Kafka SASL/SCRAM password",
)

endpoints: str = Field(
description="A list of endpoints used to connect to the topic",
description="A list of endpoints used to connect to the topic. A bare string on the wire, comma separated if there is more than one endpoint",
examples=["10.141.78.155:9092,10.141.78.62:9092,10.141.78.186:9092"],
title="Kafka server endpoints",
)

consumer_group_prefix: str | None = Field(
None,
alias="consumer-group-prefix",
description="A prefix for wildcard consumer-group IDs that have been granted permissions",
description="A prefix for wildcard consumer-group IDs that have been granted permissions. A bare string on the wire",
examples=["relation-14-"],
title="Kafka consumer group prefix",
)

zookeeper_uris: str | None = Field(
None,
alias="consumer-group-prefix",
description="A comma-seperated list of Zookeeper server URIs, and Kafka cluster zNode",
alias="zookeeper-uris",
description="A comma-seperated list of Zookeeper server URIs, and Kafka cluster zNode. A bare string on the wire",
examples=["10.141.78.155:2181,10.141.78.62:2181,10.141.78.186:2181/kafka"],
title="Zookeeper URIs",
)

entity_name: str | None = Field(
None,
alias="entity-name",
description="Name for the requested custom entity",
description="Name for the requested custom entity. A bare string on the wire, but usually delivered in a Juju secret instead, in which case the key is absent from the databag",
examples=["custom-role"],
title="Entity name",
)

entity_password: str | None = Field(
None,
alias="entity-password",
description="Password for the requested custom entity",
description="Password for the requested custom entity. A bare string on the wire, but usually delivered in a Juju secret instead, in which case the key is absent from the databag",
examples=["alphanum-32byte-random"],
title="Entity password",
)


class KafkaRequirerData(BaseModel):
class RequirerAppData(_BareStringDatabag):
"""The databag for the requirer side of this interface."""

model_config = ConfigDict(strict=True, populate_by_name=True)

topic: str = Field(
description="The topic name access requested by the requirer",
description="The topic name access requested by the requirer. A bare string on the wire",
examples=["topic-1", "appname-*"],
title="Topic name",
)

consumer_group_prefix: str | None = Field(
None,
alias="consumer-group-prefix",
description="A prefix for wildcard consumer-group IDs that have been granted permissions",
description="A prefix for wildcard consumer-group IDs that have been granted permissions. A bare string on the wire",
examples=["relation-14-"],
title="Kafka consumer group prefix",
)

extra_user_roles: str | None = Field(
None,
alias="extra-user-roles",
description="Any extra user roles requested by the requirer",
description="Any extra user roles requested by the requirer. A bare string on the wire, comma separated if there is more than one role",
examples=[
"consumer",
"producer",
Expand All @@ -113,23 +125,23 @@ class KafkaRequirerData(BaseModel):
extra_group_roles: str | None = Field(
None,
alias="extra-group-roles",
description="Any extra group roles requested by the requirer",
description="Any extra group roles requested by the requirer. A bare string on the wire, comma separated if there is more than one role",
examples=["charmed_read"],
title="Extra group roles",
)

entity_type: str | None = Field(
None,
alias="entity-type",
description="Type of the requested entity (user / group)",
description="Type of the requested entity (user / group). A bare string on the wire",
examples=["USER", "GROUP"],
title="Entity type",
)

entity_permissions: str | None = Field(
None,
alias="entity-permissions",
description="List of permissions to assign to the custom entity, in JSON format",
description="List of permissions to assign to the custom entity, in JSON format. The library treats this as an opaque string, so it is written to the databag as-is rather than being re-encoded",
examples=[
"[{\"resource_name\": \"messages\", \"resource_type\": \"TOPIC\", \"privileges\": [\"READ\"]}]"
],
Expand All @@ -148,13 +160,5 @@ def capitalize(cls, value: str) -> str:
return value


class ProviderSchema(DataBagSchema):
"""The schema for the provider side of this interface."""

app: KafkaProviderData


class RequirerSchema(DataBagSchema):
"""The schema for the requirer side of this interface."""

app: KafkaRequirerData
ProviderUnitData = None
RequirerUnitData = None
53 changes: 30 additions & 23 deletions interfaces/karapace_client/interface/v0/schema.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,34 @@
"""This file defines the schemas for the provider and requirer sides of the karapace_client interface.

It must expose two interfaces.schema_base.DataBagSchema subclasses called:
- ProviderSchema
- RequirerSchema
"""
"""This file defines the schemas for the provider and requirer sides of the karapace_client interface."""

from enum import Enum

from interface_tester.schema_base import DataBagSchema
from pydantic import BaseModel, Field, field_validator
from pydantic import BaseModel, ConfigDict, Field, field_validator


class ExtraUserRole(str, Enum):
admin = "admin"
user = "user"


class KarapaceProviderData(BaseModel):
class _BareStringDatabag(BaseModel):
"""Base class for databag models that don't strictly JSON encode all entries."""

@staticmethod
def __juju_decoder__(value: str) -> str:
"""Pass Juju's string through unmodified to be decoded by individual field validators."""
return value

@staticmethod
def __juju_encoder__(value: str | None) -> str:
"""Convert `None` to "", erasing the value; Ops will error on a non-string."""
return "" if value is None else value


class ProviderAppData(_BareStringDatabag):
"""The databag for the provider side of this interface."""

model_config = ConfigDict(strict=True, populate_by_name=True)

subject: str = Field(
description="The subject that has been made available to the relation user. Name defined in the Requirer's subject field",
examples=["subject-1"],
Expand All @@ -38,7 +48,7 @@ class KarapaceProviderData(BaseModel):
)

endpoints: str = Field(
description="A list of endpoints used to connect to the subject",
description="A list of endpoints used to connect to the subject, comma separated on the wire",
examples=["10.141.78.155:8082,10.141.78.62:8082,10.141.78.186:8082"],
title="Karapace server endpoints",
)
Expand All @@ -60,9 +70,11 @@ class KarapaceProviderData(BaseModel):
)


class KarapaceRequirerData(BaseModel):
class RequirerAppData(_BareStringDatabag):
"""The databag for the requirer side of this interface."""

model_config = ConfigDict(strict=True, populate_by_name=True)

subject: str = Field(
description="The subject name access requested by the requirer",
examples=["subject-1"],
Expand Down Expand Up @@ -96,7 +108,7 @@ class KarapaceRequirerData(BaseModel):
entity_permissions: str | None = Field(
None,
alias="entity-permissions",
description="List of permissions to assign to the custom entity, in JSON format",
description="List of permissions to assign to the custom entity, in JSON format. Written to the databag as an opaque string",
examples=[
"[{\"resource_name\": \"schemas\", \"resource_type\": \"SUBJECT\", \"privileges\": [\"READ\"]}]"
],
Expand All @@ -105,7 +117,10 @@ class KarapaceRequirerData(BaseModel):

@field_validator("extra_user_roles", mode="before")
@classmethod
def extra_user_roles_validator(cls, value: str) -> str:
def extra_user_roles_validator(cls, value: str | None) -> str | None:
if value is None:
return value

try:
_role = ExtraUserRole(value)
except ValueError:
Expand All @@ -114,13 +129,5 @@ def extra_user_roles_validator(cls, value: str) -> str:
return value


class ProviderSchema(DataBagSchema):
"""The schema for the provider side of this interface."""

app: KarapaceProviderData


class RequirerSchema(DataBagSchema):
"""The schema for the requirer side of this interface."""

app: KarapaceRequirerData
ProviderUnitData = None
RequirerUnitData = None
Loading