Patterns

When creating charms, layers, or interface layers with reactive, some common patterns can come up. This page documents some of them.

Request / Response

A common pattern in interface layers is for one charm to generate individual requests, which then need to be paired with a specific response from the other charm. This can be tricky to accomplish with relation data, due to the fact that a given unit can only publish its data for the entire relation, rather than a specific remote unit, plus the fact that a given unit may want to submit multiple requests. The framework provides some base classes to assist with this pattern.

An interface layer would first define a request and response type, which inherit from BaseRequest and BaseResponse respectively, and which each define a set of Field attributes to hold the data for the request and response. Each field can provide a description, for documentation purposes.

Note

The request class must explicitly point to the class which implements the associated response, via the RESPONSE_CLASS attribute, so that the correct class can be used when creating responses to requests.

For example:

from charms.reactive import BaseRequest, BaseResponse, Field


class CertResponse(BaseResponse):
    signed_cert = Field(description="""
                        The text of the public certificate signed by the CA.
                        """)

class CertRequest(BaseRequest):
    RESPONSE_CLASS = CertResponse  # point to response implementation

    csr_data = Field(description="""
                     The text of the generated Certificate Signing Request.
                     """)

Then, the interface layer would define endpoint classes which inherit from RequesterEndpoint and ResponderEndpoint rather than directly from Endpoint. These classes would point to the appropriate request implementation to use via the REQUEST_CLASS attribute, and they would inherit various properties and methods for interacting with the requests and responses (although it may make sense for them to wrap some of these with methods of their own more specialized for their specific needs).

For example:

from charms.reactive import RequesterEndpoint, ResponderEndpoint


class CertRequester(RequesterEndpoint):
    REQUEST_CLASS = CertRequest  # point to request implementation

    @property
    def related_cas(self):
        """
        A list of the related CAs which can sign certs.
        """
        return self.relations

    def send_csr(self, related_ca, csr_data):
        """
        Send a CSR to the specified related CA.

        Returns the created request.
        """
        return CertRequest.create(relation=related_ca,
                                  csr_data=csr_data)


class CertResponder(ResponderEndpoint):
    REQUEST_CLASS = CertRequest  # point to request implementation

    # no additional implementation needed beyond the inherited properties / methods

Charms using this interface layer could then submit requests and provide responses.

For example, a client charm might look something like:

@when('endpoint.certs.joined')
@when_not('charm.cert_requested')
def request_cert():
    cert_provider = endpoint_from_name('certs')
    if len(cert_provider.related_cas) == 0:
        return
    if len(cert_provider.related_cas) > 1:
        status.blocked('Too many CAs')
        return
    ca = cert_provider.related_cas[0]
    csr_data = generate_csr()
    request = cert_provider.send_csr(ca, csr_data)
    unitdata.kv().set('current_cert_request', request.request_id)  # for reissues
    set_flag('charm.cert_requested')

@when('endpoint.certs.all_responses')
def write_cert():
    cert_provider = endpoint_from_name('certs')
    current_request = unitdata.kv().get('current_cert_request')  # handle reissues
    response = cert_provider.response_by_field(request_id=current_request)
    CERT_PATH.write_text(response.signed_cert)

And the corresponding provider charm might look something like:

@when('endpoint.cert_clients.new_requests')
def sign_certs():
    cert_clients = endpoint_from_name('cert_clients')
    for request in cert_clients.new_requests:
        signed_cert = sign_cert(request.csr_data)
        request.respond(signed_cert=signed_cert)