Skip to content

Commit

Permalink
Merge pull request #554 from msqd/custom_controllers
Browse files Browse the repository at this point in the history
[FEATURE] Custom controllers for proxy
  • Loading branch information
hartym authored Oct 28, 2024
2 parents 1429472 + 12b5097 commit b9c0575
Show file tree
Hide file tree
Showing 17 changed files with 358 additions and 148 deletions.
33 changes: 29 additions & 4 deletions docs/apps/proxy/examples/full.yml
Original file line number Diff line number Diff line change
@@ -1,21 +1,46 @@
proxy:
endpoints:
- name: httpbins # a unique name for the endpoint, used to reference it in code, rules and dashboard
description: An informative description of the endpoint (optional)
port: 4000 # the local listening port for this endpoint
- # An identifier for this local endpoint, used to reference it in code, rules and
# dashboard. Must be unique for a given harp instance.
name: httpbins

# a human-readable description of the endpoint (optional)
description: A very informative description

# the local listening port for this endpoint
port: 4000

# the controller to use for this endpoint, "null" means default but you can specify
# a custom controller. Most of the time you will want to use the default controller
# and thus do not include this line.
controller: null

# the remote to proxy to, which can be anything from a single url to a complex
# multipool configuration with # defaults and fallbacks, healthcheck probes, etc.
remote:
min_pool_size: 2 # the minimum number of endpoints to try to keep in the active pool
# the minimum number of endpoints to try to keep in the active pool. If the number
# of active endpoints falls below this number, the proxy will attempt to bring them
# back up to this number by including endpoints from the fallback pool.
min_pool_size: 2

# Endpoints
endpoints:
- url: "https://api1.example.com/"
- url: "https://api2.example.com/"
- url: "https://fallback.example.com/"
pools: [fallback]
liveness:
type: ignore

# Liveness settings for all endpoints, if not overriden by a specific endpoint's
# liveness.
liveness:
type: naive
failure_threshold: 2
success_threshold: 2

# A periodic probe to try to close the circuit breaker even if it's not actively
# queried by the end user.
probe:
method: GET
path: /healthz
Expand Down
3 changes: 2 additions & 1 deletion docs/apps/proxy/examples/reference.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
proxy:
endpoints:
- description: null
- controller: null
description: null
name: my-endpoint
port: 4000
remote:
Expand Down
34 changes: 20 additions & 14 deletions docs/apps/proxy/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Proxy
.. versionadded:: 0.5

The ``harp_apps.proxy`` application provides the core proxy features for HARP and includes the configuration logic for
endpoints.
endpoints (the mapping between local ports and remote urls, including how to handle them).

.. toctree::
:hidden:
Expand All @@ -20,46 +20,52 @@ endpoints.
Setup
:::::

The proxy application is enabled by default when using the harp start ... or harp server ... commands.

You can disable it with the --disable proxy option, although this is generally not recommended.
The proxy application is enabled by default when using the harp start ... or harp server ... commands. You can disable
it with the --disable proxy option, although this will most probably result in an useless system.


Configuration
:::::::::::::

Shorthand syntax example:
Minimal example
---------------

.. literalinclude:: ./examples/full-shorthand.yml
:language: yaml

Full example:
.. seealso::

:doc:`📃 Proxy Configuration Reference <settings>`


Full example
------------

.. literalinclude:: ./examples/full.yml
:language: yaml

**Reference**
.. seealso::

* :class:`proxy (ProxySettings) <harp_apps.proxy.settings.ProxySettings>`
* :class:`proxy.endpoints[] (ProxyEndpoint) <harp_apps.proxy.settings.ProxyEndpoint>`
* :class:`proxy.endpoints[].remote (HttpRemote) <harp_apps.proxy.models.remotes.HttpRemote>`
* :class:`proxy.endpoints[].remote.endpoints[] (HttpEndpoint) <harp_apps.proxy.models.remotes.HttpEndpoint>`
* :class:`proxy.endpoints[].remote.probe (HttpProbe) <harp_apps.proxy.models.remotes.HttpProbe>`
:doc:`📃 Proxy Configuration Reference <settings>`


Command line
::::::::::::

It is also possible to add endpoints using the command line:
It is also possible to add endpoints using the command line. This is available for quick tests but should not be used as
a permanent solution.

.. code-block:: bash
harp start --endpoint starwars=1234:https://swapi.dev/
.. warning::

The current CLI syntax is hackish and limited, the syntax will most probably change in the future.

You can use multiple ``--endpoint ...`` arguments and the option is available for all server-like commands
(``harp start ...``, ``harp server ...``, ...).


.. warning::

For now, endpoints does not support subpaths on remote side. For exemple: http://example.com/ is supported as
Expand Down
8 changes: 8 additions & 0 deletions docs/apps/proxy/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
"description": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"default": null
},
"controller": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"default": null
}
},
"required": ["name", "port"],
Expand Down Expand Up @@ -108,6 +112,10 @@
"anyOf": [{ "type": "string" }, { "type": "null" }],
"default": null
},
"controller": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"default": null
},
"remote": {
"anyOf": [{ "$ref": "#/$defs/RemoteSettings" }, { "type": "null" }],
"default": null
Expand Down
10 changes: 5 additions & 5 deletions docs/apps/proxy/settings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ Implementation (python): :class:`RemoteProbeSettings <harp_apps.proxy.settings.R
Can be set at the remote enpoint level or at the remote level. If all levels are set to inherit, will use a naive
implementation by default.

Implementation (python): :class:`BaseLivenessSettings <harp_apps.proxy.settings.liveness.BaseLivenessSettings>`
Implementation (python): :class:`BaseLivenessSettings <harp_apps.proxy.settings.liveness.base.BaseLivenessSettings>`

.. jsonschema:: ./schema.json
:pointer: /$defs/BaseLivenessSettings
Expand All @@ -79,31 +79,31 @@ This is the base type, actual implementation will depend on choosen type, docume
Inherit
.......

Implementation (python): :class:`InheritLivenessSettings <harp_apps.proxy.settings.liveness.InheritLivenessSettings>`
Implementation (python): :class:`InheritLivenessSettings <harp_apps.proxy.settings.liveness.base.BaseLivenessSettings>`

.. jsonschema:: ./schema.json
:pointer: /$defs/InheritLivenessSettings

Ignore
......

Implementation (python): :class:`IgnoreLivenessSettings <harp_apps.proxy.settings.liveness.IgnoreLivenessSettings>`
Implementation (python): :class:`IgnoreLivenessSettings <harp_apps.proxy.settings.liveness.base.BaseLivenessSettings>`

.. jsonschema:: ./schema.json
:pointer: /$defs/IgnoreLivenessSettings

Naive
.....

Implementation (python): :class:`NaiveLivenessSettings <harp_apps.proxy.settings.liveness.NaiveLivenessSettings>`
Implementation (python): :class:`NaiveLivenessSettings <harp_apps.proxy.settings.liveness.base.BaseLivenessSettings>`

.. jsonschema:: ./schema.json
:pointer: /$defs/NaiveLivenessSettings

Leaky Bucket
............

Implementation (python): :class:`LeakyBucketLivenessSettings <harp_apps.proxy.settings.liveness.LeakyBucketLivenessSettings>`
Implementation (python): :class:`LeakyBucketLivenessSettings <harp_apps.proxy.settings.liveness.base.BaseLivenessSettings>`

.. jsonschema:: ./schema.json
:pointer: /$defs/LeakyBucketLivenessSettings
Expand Down
1 change: 1 addition & 0 deletions docs/changelogs/unreleased.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ Unreleased
==========

* Config: «!include» constructor can now be used in all configuration files, not only service definitions.
* Proxy: It is now possible to override the default HttpProxyController with your own on a per-endpoint basis.
* Rules/DX: Better errors on rules failure.
* Rules/DX: Rules can now return a dict response as a shortcut to a json response.
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
''',
'harp_apps.proxy.settings.endpoint.Endpoint': '''
settings:
controller: null
description: null
name: api
port: 4000
Expand Down
2 changes: 1 addition & 1 deletion harp/config/tests/__snapshots__/test_all_examples.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@
http_client: {}
proxy:
endpoints:
- description: An informative description of the endpoint (optional)
- description: A very informative description
name: httpbins
port: 4000
remote:
Expand Down
9 changes: 7 additions & 2 deletions harp/controllers/resolvers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import TYPE_CHECKING, Optional
from typing import TYPE_CHECKING, Optional, Type

from httpx import AsyncClient
from whistle import IAsyncEventDispatcher
Expand Down Expand Up @@ -48,15 +48,20 @@ def add(
*,
http_client: AsyncClient,
dispatcher: Optional[IAsyncEventDispatcher] = None,
ControllerType: Optional[Type[HttpProxyController]] = None,
):
if endpoint.settings.name in self._endpoints:
raise RuntimeError(f"Endpoint «{endpoint.settings.name}» already exists.")

if endpoint.settings.port in self._ports:
raise RuntimeError(f"Port «{endpoint.settings.port}» already in use.")

ControllerType = ControllerType or HttpProxyController
if not issubclass(ControllerType, HttpProxyController):
raise RuntimeError(f"Controller «{ControllerType}» must be a subclass of HttpProxyController.")

self._endpoints[endpoint.settings.name] = endpoint
controller = HttpProxyController(
controller = ControllerType(
endpoint.remote,
dispatcher=dispatcher,
http_client=http_client,
Expand Down
2 changes: 2 additions & 0 deletions harp_apps/dashboard/frontend/types/harp_apps.proxy.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ declare namespace Apps.Proxy {
name: string;
port: number;
description?: string | null;
controller?: string | null;
remote?: RemoteSettings | null;
}
/**
Expand Down Expand Up @@ -142,6 +143,7 @@ declare namespace Apps.Proxy {
name: string;
port: number;
description?: string | null;
controller?: string | null;
}
export interface Endpoint {
settings: EndpointSettings;
Expand Down
7 changes: 6 additions & 1 deletion harp_apps/dashboard/tests/controllers/test_system_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,12 @@ async def test_get_proxy(self, controller: SystemController):
"min_pool_size": 1,
},
},
"settings": {"description": None, "name": "api", "port": 4000},
"settings": {
"name": "api",
"description": None,
"port": 4000,
"controller": None,
},
}
]
}
Expand Down
8 changes: 7 additions & 1 deletion harp_apps/proxy/__app__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from harp.config import Application
from harp.config.events import OnBindEvent, OnBoundEvent, OnShutdownEvent
from harp.utils.packages import import_string
from harp.utils.services import factory

from .settings import Proxy, ProxySettings
Expand Down Expand Up @@ -41,7 +42,12 @@ async def on_bound(event: OnBoundEvent):
http_client: AsyncClient = event.provider.get(AsyncClient)

for endpoint in proxy.endpoints:
event.resolver.add(endpoint, dispatcher=event.dispatcher, http_client=http_client)
ControllerType = None
if endpoint.settings.controller is not None:
ControllerType = import_string(endpoint.settings.controller)
event.resolver.add(
endpoint, dispatcher=event.dispatcher, http_client=http_client, ControllerType=ControllerType
)

event.provider.set(
PROXY_HEALTHCHECKS_TASK,
Expand Down
Loading

0 comments on commit b9c0575

Please sign in to comment.