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
67 changes: 49 additions & 18 deletions interfaces/azure_service_principal/interface/v0/schema.py
Original file line number Diff line number Diff line change
@@ -1,58 +1,89 @@
"""This file defines the schemas for the provider and requirer sides of the azure_service_principal interface.
"""This file defines the schemas for the provider and requirer sides of the azure_service_principal interface."""

It must expose 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, SecretStr
from pydantic import BaseModel, ConfigDict, Field, SecretStr, field_serializer, field_validator


class AzureServicePrincipalProviderAppData(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):
"""Credentials for an Azure Service Principal."""

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

subscription_id: str = Field(
alias="subscription-id",
description="The unique identifier for an Azure subscription.",
examples=["12345678-1234-1234-1234-1234567890ab"],
title="Subscription ID",
)

tenant_id: str = Field(
alias="tenant-id",
description="The unique identifier of the Azure Active Directory (Entra ID) tenant.",
examples=["87654321-4321-4321-4321-ba0987654321"],
title="Tenant ID",
)

client_id: str = Field(
alias="client-id",
description="The Application (client) ID for the service principal.",
examples=["a1b2c3d4-e5f6-a7b8-c9d0-e1f2a3b4c5d6"],
title="Client ID",
)

client_secret: SecretStr = Field(
alias="client-secret",
description="The client secret for the service principal, used for authentication.",
examples=["aBcDeFgHiJkLmNoPqRsTuVwXyZ123456-~7890_"],
title="Client Secret",
)

@field_serializer("client_secret")
def _reveal(self, value: SecretStr) -> str:
"""Write the real secret to the databag rather than the masked repr."""
return value.get_secret_value()


class RequirerAppData(_BareStringDatabag):
"""The fields the requirer asks to be delivered as Juju secrets."""

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

class AzureServicePrincipalRequirerAppData(BaseModel):
requested_secrets: list[str] = Field(
alias="requested-secrets",
description="Any provider field which should be transfered as a Juju secret",
description="Any provider field which should be transfered as a Juju secret. A JSON array on the wire.",
examples=[["client-id", "client-secret"]],
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 this interface."""

app: AzureServicePrincipalProviderAppData

@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 this interface."""

app: AzureServicePrincipalRequirerAppData
ProviderUnitData = None
RequirerUnitData = None
2 changes: 0 additions & 2 deletions interfaces/azure_storage/interface/v0/interface.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ description: |
providers:
- name: azure-storage-integrator
url: https://github.com/canonical/object-storage-integrators
test_setup:
charm_root: azure_storage

requirers:
- name: spark-integration-hub-k8s
Expand Down
85 changes: 65 additions & 20 deletions interfaces/azure_storage/interface/v0/schema.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
"""This file defines the schemas for the provider and requirer sides of the zookeeper_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 azure_storage interface."""

import json
import pathlib
from enum import Enum
from pathlib import Path
from typing import Any

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


class ConnectionProtocolEnum(str, Enum):
Expand All @@ -18,33 +15,55 @@ class ConnectionProtocolEnum(str, Enum):
adls_gen2_secure = "abfss"


class AzureStorageProviderAppData(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)

container: str = Field(
description="The name of the Azure storage container provided by the provider.",
examples=["mycontainer"],
title="Container",
)

storage_account: str = Field(
alias="storage-account",
description="The name of Azure storage account.",
examples=["test-storage-account"],
title="Storage account",
)

connection_protocol: ConnectionProtocolEnum = Field(
alias="connection-protocol",
description="The connection protocol to be used to connect to Azure Storage.",
examples=["wasb", "wasbs", "abfs", "abfss"],
default=ConnectionProtocolEnum.adls_gen2_secure,
strict=False,
title="Connection protocol",
)

secret_key: SecretStr = Field(
alias="secret-key",
description="Secret key corresponding to the storage account for connecting to the object storage.",
examples=["random-secret-key"],
title="Secret key",
)

path: Path = Field(
path: str = Field(
description="The path inside the container to store objects.",
examples=["foo/bar"],
title="Path",
Expand All @@ -56,8 +75,29 @@ class AzureStorageProviderAppData(BaseModel):
title="Endpoint URL",
)

@field_serializer("connection_protocol")
def _dump_enum(self, value: ConnectionProtocolEnum) -> str:
"""Write the enum's wire string rather than the enum member."""
return value.value

@field_serializer("secret_key")
def _reveal(self, value: SecretStr) -> str:
"""Write the real secret to the databag rather than the masked repr."""
return value.get_secret_value()

@field_validator("path")
@classmethod
def _check_path(cls, value: str) -> str:
"""Check the value is a usable path, keeping any trailing slash."""
pathlib.PurePosixPath(value) # raises if it isn't a usable path
return value


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

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

class AzureStorageRequirerAppData(BaseModel):
container: str = Field(
description="The name of the container that's requested by the requirer.",
examples=["mycontainer"],
Expand All @@ -66,19 +106,24 @@ class AzureStorageRequirerAppData(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", "uris"]],
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 this interface."""

app: AzureStorageProviderAppData

@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 this interface."""

app: AzureStorageRequirerAppData
ProviderUnitData = None
RequirerUnitData = None
Empty file.
44 changes: 0 additions & 44 deletions interfaces/azure_storage/interface/v0/tests/test_provider.py

This file was deleted.

7 changes: 0 additions & 7 deletions interfaces/azure_storage/ruff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,3 @@ quote-style = "preserve"
"D", # docs
"E501", # line too long
]
"./interface/v*/tests/*.py" = [
"CPY", # copyright
"D", # docs
"S", # security
"E501", # line too long
"F841", # assignment to unused variable
]
Loading
Loading