Packages
livebook
0.19.6
0.19.8
0.19.7
0.19.6
0.19.5
0.19.4
0.19.3
0.19.2
0.19.1
0.19.0
0.18.6
0.18.5
0.18.4
0.18.3
0.18.2
0.18.1
0.18.0
0.17.3
0.17.2
0.17.1
0.17.0
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.7
0.14.6
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.14.0-rc.1
0.14.0-rc.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.1
0.12.0
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.0
0.9.3
0.9.2
0.9.1
0.9.0
0.8.2
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.6.3
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.2
0.3.1
0.3.0
0.2.3
0.2.2
0.2.1
0.2.0
0.1.2
0.1.1
0.1.0
Automate code & data workflows with interactive notebooks
Current section
Files
Jump to
Current section
Files
lib/livebook/runtime/k8s.ex
defmodule Livebook.Runtime.K8s do
# A runtime backed by a Kubernetes Pod managed by Livebook.
#
# This runtime uses the same concepts as the Fly runtime. In this
# case, we start a Pod in a Kubernetes cluster and use kubectl to
# proxy a local port to the distribution port of the remote node.
# See `Livebook.Runtime.Fly` for more design details.
defstruct [:config, :node, :server_pid, :lv_pid, :pod_name]
use GenServer, restart: :temporary
require Logger
alias Livebook.Runtime.RemoteUtils
alias Livebook.K8s.Pod
@type t :: %__MODULE__{
node: node() | nil,
server_pid: pid() | nil,
lv_pid: pid(),
pod_name: String.t() | nil
}
@type config :: %{
context: String.t(),
namespace: String.t(),
docker_tag: String.t(),
pod_template: String.t(),
pvc_name: String.t() | nil
}
@doc """
Returns a new runtime instance.
"""
@spec new(map()) :: t()
def new(config) do
%__MODULE__{config: config, lv_pid: self()}
end
def __connect__(runtime) do
{:ok, pid} =
DynamicSupervisor.start_child(Livebook.RuntimeSupervisor, {__MODULE__, {runtime, self()}})
pid
end
@doc false
def start_link({runtime, caller}) do
GenServer.start_link(__MODULE__, {runtime, caller})
end
@impl true
def init({runtime, caller}) do
state = %{primary_ref: nil}
{:ok, state, {:continue, {:init, runtime, caller}}}
end
@impl true
def handle_continue({:init, runtime, caller}, state) do
config = runtime.config
%{namespace: namespace, context: context} = config
within_kubernetes? = System.get_env("KUBERNETES_SERVICE_HOST") != nil
{node_base, local_port} =
if within_kubernetes? do
# When already running within Kubernetes we don't need the
# proxy, the node is reachable directly
{"k8s_runtime", nil}
else
local_port = RemoteUtils.get_free_port!()
{"remote_runtime_#{local_port}", local_port}
end
kubeconfig =
Kubereq.Kubeconfig.Default
|> Kubereq.Kubeconfig.load()
|> Kubereq.Kubeconfig.set_current_context(context)
runtime_data = RemoteUtils.encode_runtime_data(node_base)
parent = self()
{:ok, watcher_pid} =
DynamicSupervisor.start_child(
Livebook.RuntimeSupervisor,
{Task, fn -> watcher(parent, kubeconfig, config) end}
)
with {:ok, pod_name} <-
with_log(caller, "create pod", fn ->
create_pod(kubeconfig, config, runtime_data)
end),
_ <- send(watcher_pid, {:pod_created, pod_name}),
{:ok, pod_ip} <-
with_pod_events(caller, "waiting for pod", kubeconfig, namespace, pod_name, fn ->
await_pod_ready(kubeconfig, namespace, pod_name)
end),
child_node <- :"#{node_base}@#{pod_ip}",
:ok <-
(if within_kubernetes? do
:ok
else
with_log(caller, "start proxy", fn ->
k8s_forward_port(context, local_port, pod_name, namespace)
end)
end),
:ok <-
with_log(caller, "connect to node", fn ->
RemoteUtils.connect(child_node)
end),
%{pid: primary_pid} <- RemoteUtils.fetch_runtime_info(child_node) do
primary_ref = Process.monitor(primary_pid)
server_pid =
with_log(caller, "initialize node", fn ->
RemoteUtils.initialize_node(child_node)
end)
send(primary_pid, :node_initialized)
send(watcher_pid, :done)
runtime = %{runtime | node: child_node, server_pid: server_pid, pod_name: pod_name}
send(caller, {:runtime_connect_done, self(), {:ok, runtime}})
{:noreply, %{state | primary_ref: primary_ref}}
else
{:error, error} ->
send(caller, {:runtime_connect_done, self(), {:error, error}})
{:stop, :shutdown, state}
end
end
@impl true
def handle_info({:DOWN, ref, :process, _pid, _reason}, state) when ref == state.primary_ref do
{:stop, :shutdown, state}
end
def handle_info({port, _message}, state) when is_port(port) do
{:noreply, state}
end
defp with_pod_events(caller, name, kubeconfig, namespace, pod_name, fun) do
with_log(caller, name, fn ->
runtime_pid = self()
event_watcher_pid =
spawn_link(fn ->
with {:ok, stream} <- Livebook.K8sAPI.watch_pod_events(kubeconfig, namespace, pod_name) do
for event <- stream do
message = Livebook.Utils.downcase_first(event.message)
send(caller, {:runtime_connect_info, runtime_pid, message})
Logger.debug(~s/[k8s runtime] Pod event: "#{message}"/)
end
end
end)
result = fun.()
Process.exit(event_watcher_pid, :normal)
result
end)
end
defp watcher(parent, kubeconfig, config) do
ref = Process.monitor(parent)
watcher_loop(%{ref: ref, config: config, kubeconfig: kubeconfig, pod_name: nil})
end
defp watcher_loop(state) do
receive do
{:DOWN, ref, :process, _pid, _reason} when ref == state.ref ->
# If the parent process is killed, we try to eagerly free the
# created resources
if pod_name = state.pod_name do
_ = Livebook.K8sAPI.delete_pod(state.kubeconfig, state.config.namespace, pod_name)
end
{:pod_created, pod_name} ->
watcher_loop(%{state | pod_name: pod_name})
:done ->
:ok
end
end
defp create_pod(kubeconfig, config, runtime_data) do
%{
pod_template: pod_template,
docker_tag: docker_tag,
pvc_name: pvc_name,
namespace: namespace
} = config
manifest =
pod_template
|> Pod.pod_from_template()
|> Pod.add_env_vars([
%{"name" => "LIVEBOOK_RUNTIME", "value" => runtime_data},
%{
"name" => "POD_IP",
"valueFrom" => %{"fieldRef" => %{"apiVersion" => "v1", "fieldPath" => "status.podIP"}}
},
%{
"name" => "POD_NAMESPACE",
"valueFrom" => %{
"fieldRef" => %{"apiVersion" => "v1", "fieldPath" => "metadata.namespace"}
}
},
%{
"name" => "POD_NAME",
"valueFrom" => %{"fieldRef" => %{"apiVersion" => "v1", "fieldPath" => "metadata.name"}}
}
])
|> Pod.set_docker_tag(docker_tag)
|> Pod.set_namespace(namespace)
|> Pod.add_container_port(RemoteUtils.remote_port())
manifest =
if pvc_name do
Pod.set_pvc_name(manifest, pvc_name)
else
manifest
end
case Livebook.K8sAPI.create_pod(kubeconfig, manifest) do
{:ok, %{name: name}} ->
{:ok, name}
{:error, %{message: message}} ->
{:error, "could not create Pod, reason: #{message}"}
end
end
defp k8s_forward_port(context, local_port, pod_name, namespace) do
with {:ok, kubectl_path} <- find_kubectl_executable() do
ports = "#{local_port}:#{RemoteUtils.remote_port()}"
# We want the proxy to accept the same protocol that we are
# going to use for distribution
bind_addr =
if Livebook.Utils.proto_dist() == :inet6_tcp do
"[::1]"
else
"127.0.0.1"
end
kubeconfig = System.get_env("KUBECONFIG") || Path.join(System.user_home!(), ".kube/config")
args =
[
"port-forward",
"--kubeconfig",
Path.expand(kubeconfig),
"--context",
context,
"-n",
namespace,
pod_name,
ports,
"--address",
bind_addr
]
port =
Port.open(
{:spawn_executable, kubectl_path},
[:binary, :hide, :stderr_to_stdout, args: args, env: []]
)
port_ref = Port.monitor(port)
result =
receive do
{^port, {:data, "Forwarding from " <> _}} ->
:ok
{^port, {:data, "Error " <> _ = message}} ->
{:error, "failed to port-forward. #{String.trim(message)}"}
{:DOWN, ^port_ref, :port, _object, reason} ->
{:error, "failed to port-forward. Process terminated, reason: #{inspect(reason)}"}
after
30_000 ->
{:error, "failed to port-forward. Timed out after 30s"}
end
Port.demonitor(port_ref, [:flush])
result
end
end
defp find_kubectl_executable() do
if path = System.find_executable("kubectl") do
{:ok, path}
else
{:error, "no kubectl executable found in PATH"}
end
end
defp await_pod_ready(kubeconfig, namespace, pod_name) do
with :ok <- Livebook.K8sAPI.await_pod_ready(kubeconfig, namespace, pod_name),
{:ok, %{ip: pod_ip}} <- Livebook.K8sAPI.get_pod(kubeconfig, namespace, pod_name) do
{:ok, pod_ip}
else
{:error, %{message: message}} ->
{:error, "failed while waiting for the Pod to start, reason: #{message}"}
end
end
defp with_log(caller, name, fun) do
send(caller, {:runtime_connect_info, self(), name})
RemoteUtils.with_log("[k8s runtime] #{name}", fun)
end
end
defimpl Livebook.Runtime, for: Livebook.Runtime.K8s do
alias Livebook.Runtime.ErlDist.RuntimeServer
def describe(runtime) do
[{"Type", "K8s Pod"}] ++
if runtime.node do
[{"Pod name", runtime.pod_name}, {"Node name", Atom.to_string(runtime.node)}]
else
[]
end
end
def connect(runtime) do
Livebook.Runtime.K8s.__connect__(runtime)
end
def take_ownership(runtime, opts) do
RuntimeServer.attach(runtime.server_pid, self(), opts)
Process.monitor(runtime.server_pid)
end
def disconnect(runtime) do
:ok = RuntimeServer.stop(runtime.server_pid)
end
def duplicate(runtime) do
Livebook.Runtime.K8s.new(runtime.config)
end
def evaluate_code(runtime, language, code, locator, parent_locators, opts) do
RuntimeServer.evaluate_code(
runtime.server_pid,
language,
code,
locator,
parent_locators,
opts
)
end
def forget_evaluation(runtime, locator) do
RuntimeServer.forget_evaluation(runtime.server_pid, locator)
end
def drop_container(runtime, container_ref) do
RuntimeServer.drop_container(runtime.server_pid, container_ref)
end
def handle_intellisense(runtime, send_to, language, request, parent_locators, node) do
RuntimeServer.handle_intellisense(
runtime.server_pid,
send_to,
language,
request,
parent_locators,
node
)
end
def read_file(runtime, path) do
RuntimeServer.read_file(runtime.server_pid, path)
end
def transfer_file(runtime, path, file_id, callback) do
RuntimeServer.transfer_file(runtime.server_pid, path, file_id, callback)
end
def relabel_file(runtime, file_id, new_file_id) do
RuntimeServer.relabel_file(runtime.server_pid, file_id, new_file_id)
end
def revoke_file(runtime, file_id) do
RuntimeServer.revoke_file(runtime.server_pid, file_id)
end
def start_smart_cell(runtime, kind, ref, attrs, parent_locators) do
RuntimeServer.start_smart_cell(runtime.server_pid, kind, ref, attrs, parent_locators)
end
def set_smart_cell_parent_locators(runtime, ref, parent_locators) do
RuntimeServer.set_smart_cell_parent_locators(runtime.server_pid, ref, parent_locators)
end
def stop_smart_cell(runtime, ref) do
RuntimeServer.stop_smart_cell(runtime.server_pid, ref)
end
def supports_dependencies?(_runtime), do: true
def has_dependencies?(runtime, dependencies) do
RuntimeServer.has_dependencies?(runtime.server_pid, dependencies)
end
def packages_source(_runtime) do
:hex
end
def put_system_envs(runtime, envs) do
RuntimeServer.put_system_envs(runtime.server_pid, envs)
end
def delete_system_envs(runtime, names) do
RuntimeServer.delete_system_envs(runtime.server_pid, names)
end
def restore_transient_state(runtime, transient_state) do
RuntimeServer.restore_transient_state(runtime.server_pid, transient_state)
end
def register_clients(runtime, clients) do
RuntimeServer.register_clients(runtime.server_pid, clients)
end
def unregister_clients(runtime, client_ids) do
RuntimeServer.unregister_clients(runtime.server_pid, client_ids)
end
def fetch_proxy_handler_spec(runtime) do
RuntimeServer.fetch_proxy_handler_spec(runtime.server_pid)
end
def disconnect_node(runtime, node) do
RuntimeServer.disconnect_node(runtime.server_pid, node)
end
end