Skip to content

Commit

Permalink
Add Kino.Proxy module (#431)
Browse files Browse the repository at this point in the history
Co-authored-by: Jonatan Kłosko <[email protected]>
Co-authored-by: José Valim <[email protected]>
  • Loading branch information
3 people authored May 27, 2024
1 parent 0851a3f commit 0bd3bd0
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 0 deletions.
9 changes: 9 additions & 0 deletions lib/kino/bridge.ex
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,15 @@ defmodule Kino.Bridge do
match?({:ok, _}, io_request(:livebook_get_evaluation_file))
end

@doc """
Requests the child spec for proxy handler with the given function.
"""
@spec get_proxy_handler_child_spec((Plug.Conn.t() -> Plug.Conn.t())) ::
{:ok, {module(), term()}} | request_error()
def get_proxy_handler_child_spec(fun) do
io_request({:livebook_get_proxy_handler_child_spec, fun})
end

defp io_request(request) do
gl = Process.group_leader()
ref = Process.monitor(gl)
Expand Down
69 changes: 69 additions & 0 deletions lib/kino/proxy.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
defmodule Kino.Proxy do
@moduledoc """
Functionality for handling proxy requests forwarded from Livebook.
Livebook proxies requests at the following paths:
* `/sessions/:id/proxy/*path` - a notebook session
* `/apps/:slug/:session_id/proxy/*path` - a specific app session
* `/apps/:slug/proxy/*path` - generic app path, only supported for
single-session apps. If the app has automatic shutdowns enabled
and it is not currently running, it will be automatically started
You can define a custom listener to handle requests at these paths.
The listener receives a `Plug.Conn` and it should use the `Plug` API
to send the response, for example:
Kino.Proxy.listen(fn conn ->
Plug.Conn.send_resp(conn, 200, "hello")
end
> #### Plug {: .info}
>
> In order to use this feature, you need to add `:plug` as a dependency.
## Examples
Using the proxy feature, we can use Livebook apps to build APIs.
For example, we could provide a data export endpoint:
data = <<...>>
token = "auth-token"
Kino.Proxy.listen(fn
%{path_info: ["export", "data"]} = conn ->
["Bearer " <> ^token] = Plug.Conn.get_req_header(conn, "authorization")
conn
|> Plug.Conn.put_resp_header("content-type", "application/csv")
|> Plug.Conn.send_resp(200, data)
conn ->
conn
|> Plug.Conn.put_resp_header("content-type", "application/text")
|> Plug.Conn.send_resp(200, "use /export/data to get extract the report data")
end)
Once deployed as an app, the user would be able to export the data
by sending a request to `/apps/:slug/proxy/export/data`.
"""

@doc """
Registers a request listener.
Expects the listener to be a function that handles a request
`Plug.Conn`.
"""
@spec listen((Plug.Conn.t() -> Plug.Conn.t())) :: DynamicSupervisor.on_start_child()
def listen(fun) when is_function(fun, 1) do
case Kino.Bridge.get_proxy_handler_child_spec(fun) do
{:ok, child_spec} ->
Kino.start_child(child_spec)

{:request_error, reason} ->
raise "failed to access the proxy handler child spec, reason: #{inspect(reason)}"
end
end
end
1 change: 1 addition & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ defmodule Kino.MixProject do
{:table, "~> 0.1.2"},
{:fss, "~> 0.1.0"},
{:nx, "~> 0.1", optional: true},
{:plug, "~> 1.0", optional: true},
{:ex_doc, "~> 0.28", only: :dev, runtime: false}
]
end
Expand Down
4 changes: 4 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@
"makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"},
"makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"},
"makeup_erlang": {:hex, :makeup_erlang, "0.1.5", "e0ff5a7c708dda34311f7522a8758e23bfcd7d8d8068dc312b5eb41c6fd76eba", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "94d2e986428585a21516d7d7149781480013c56e30c6a233534bedf38867a59a"},
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
"nx": {:hex, :nx, "0.4.0", "2ec2cebec6a9ac8a3d5ae8ef79345cf92f37f9018d50817684e51e97b86f3d36", [:mix], [{:complex, "~> 0.4.2", [hex: :complex, repo: "hexpm", optional: false]}], "hexpm", "bab955768dadfe2208723fbffc9255341b023291f2aabcbd25bf98167dd3399e"},
"plug": {:hex, :plug, "1.16.0", "1d07d50cb9bb05097fdf187b31cf087c7297aafc3fed8299aac79c128a707e47", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cbf53aa1f5c4d758a7559c0bd6d59e286c2be0c6a1fac8cc3eee2f638243b93e"},
"plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
"table": {:hex, :table, "0.1.2", "87ad1125f5b70c5dea0307aa633194083eb5182ec537efc94e96af08937e14a8", [:mix], [], "hexpm", "7e99bc7efef806315c7e65640724bf165c3061cdc5d854060f74468367065029"},
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
}

0 comments on commit 0bd3bd0

Please sign in to comment.