Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Kino.Proxy module #431

Merged
merged 10 commits into from
May 27, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
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
81 changes: 81 additions & 0 deletions lib/kino/proxy.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
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 sessions
jonatanklosko marked this conversation as resolved.
Show resolved Hide resolved

* `/apps/:slug/:session_id/proxy/*path` - a specific app session

* `/apps/:slug/proxy/*path` - generic app path, only supported for
single-session apps
jonatanklosko marked this conversation as resolved.
Show resolved Hide resolved

You can define a custom listener to handle requests at these paths.
The listener receives a `Plug.Conn` and should use the `Plug` API
jonatanklosko marked this conversation as resolved.
Show resolved Hide resolved
to send the response, for example:

Kino.Proxy.listen(fn conn ->
Plug.Conn.send_resp(conn, 200, "hello")
end

The following `Plug.Conn` fields are set:

* `:host`
* `:method`
* `:owner`
* `:port`
* `:remote_ip`
* `:query_string`
* `:path_info`
* `:scheme`
* `:script_name`
* `:req_headers`
jonatanklosko marked this conversation as resolved.
Show resolved Hide resolved
jonatanklosko marked this conversation as resolved.
Show resolved Hide resolved

> #### Plug {: .info}
>
> In order to use this feature, you need to add `:plug` as a dependency.

## Examples

Using the proxy feature, you can use Livebook apps to build APIs.
jonatanklosko marked this conversation as resolved.
Show resolved Hide resolved
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"},
}
Loading