mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-10-27 13:56:21 +08:00
316 lines
8.1 KiB
Elixir
316 lines
8.1 KiB
Elixir
defmodule Livebook.Hubs do
|
|
alias Livebook.FileSystem
|
|
alias Livebook.Storage
|
|
alias Livebook.Hubs.{Broadcasts, Metadata, Personal, Provider, Team}
|
|
alias Livebook.Secrets.Secret
|
|
|
|
require Logger
|
|
|
|
@namespace :hubs
|
|
@supervisor Livebook.HubsSupervisor
|
|
|
|
@type connected_hub :: %{
|
|
required(:pid) => pid(),
|
|
required(:hub) => Provider.t()
|
|
}
|
|
@type connected_hubs :: list(connected_hub())
|
|
|
|
@doc """
|
|
Gets a list of hubs from storage.
|
|
"""
|
|
@spec get_hubs() :: list(Provider.t())
|
|
def get_hubs do
|
|
for fields <- Storage.all(@namespace) do
|
|
to_struct(fields)
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Gets a list of metadata from storage.
|
|
"""
|
|
@spec get_metadata() :: list(Metadata.t())
|
|
def get_metadata do
|
|
for hub <- get_hubs() do
|
|
Provider.to_metadata(hub)
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Gets one hub from storage.
|
|
"""
|
|
@spec fetch_hub(String.t()) :: {:ok, Provider.t()} | :error
|
|
def fetch_hub(id) do
|
|
with {:ok, data} <- Storage.fetch(@namespace, id) do
|
|
{:ok, to_struct(data)}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Gets one hub from storage.
|
|
|
|
Raises `Livebook.Storage.NotFoundError` if the hub does not exist.
|
|
"""
|
|
@spec fetch_hub!(String.t()) :: Provider.t()
|
|
def fetch_hub!(id) do
|
|
Storage.fetch!(@namespace, id) |> to_struct()
|
|
end
|
|
|
|
@doc """
|
|
Checks if hub already exists.
|
|
"""
|
|
@spec hub_exists?(String.t()) :: boolean()
|
|
def hub_exists?(id) do
|
|
case Storage.fetch(@namespace, id) do
|
|
:error -> false
|
|
{:ok, _} -> true
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Saves a new hub to the configured ones.
|
|
"""
|
|
@spec save_hub(Provider.t()) :: Provider.t()
|
|
def save_hub(struct) do
|
|
attributes = Provider.dump(struct)
|
|
:ok = Storage.insert(@namespace, struct.id, Map.to_list(attributes))
|
|
:ok = connect_hub(struct)
|
|
:ok = Broadcasts.hub_changed(struct.id)
|
|
|
|
struct
|
|
end
|
|
|
|
@doc """
|
|
Deletes a hub with the given id.
|
|
"""
|
|
@spec delete_hub(String.t()) :: :ok
|
|
def delete_hub(id) do
|
|
with {:ok, hub} <- fetch_hub(id) do
|
|
true = Provider.type(hub) != "personal"
|
|
:ok = maybe_unset_default_hub(hub.id)
|
|
:ok = Storage.delete(@namespace, id)
|
|
:ok = Broadcasts.hub_deleted(hub.id)
|
|
:ok = disconnect_hub(hub)
|
|
end
|
|
|
|
:ok
|
|
end
|
|
|
|
@spec set_default_hub(String.t()) :: :ok
|
|
def set_default_hub(id) do
|
|
with {:ok, hub} <- fetch_hub(id) do
|
|
:ok = Storage.insert(:default_hub, "default_hub", [{:default_hub, hub.id}])
|
|
end
|
|
|
|
:ok
|
|
end
|
|
|
|
@spec unset_default_hub() :: :ok
|
|
def unset_default_hub() do
|
|
:ok = Storage.delete(:default_hub, "default_hub")
|
|
end
|
|
|
|
@spec get_default_hub() :: Provider.t()
|
|
def get_default_hub() do
|
|
with {:ok, %{default_hub: id}} <- Storage.fetch(:default_hub, "default_hub"),
|
|
{:ok, hub} <- fetch_hub(id) do
|
|
hub
|
|
else
|
|
_ -> fetch_hub!(Personal.id())
|
|
end
|
|
end
|
|
|
|
defp maybe_unset_default_hub(hub_id) do
|
|
if get_default_hub().id == hub_id, do: unset_default_hub(), else: :ok
|
|
end
|
|
|
|
defp disconnect_hub(hub) do
|
|
# We use a task supervisor because the hub connection itself
|
|
# calls delete_hub (which calls this function), otherwise we deadlock.
|
|
Task.Supervisor.start_child(Livebook.TaskSupervisor, fn ->
|
|
# Since other processes may have been communicating
|
|
# with the hub, we don't want to terminate abruptly and
|
|
# make them crash, so we give it some time to shut down.
|
|
#
|
|
# The default backoff is 5.5s, so we round it down to 5s.
|
|
Process.sleep(30_000)
|
|
:ok = Provider.disconnect(hub)
|
|
end)
|
|
|
|
:ok
|
|
end
|
|
|
|
defp to_struct(%{id: "personal-" <> _} = fields) do
|
|
Provider.load(%Personal{}, fields)
|
|
end
|
|
|
|
defp to_struct(%{id: "team-" <> _} = fields) do
|
|
Provider.load(Team.new(), fields)
|
|
end
|
|
|
|
@doc """
|
|
Connects to the all available and connectable hubs.
|
|
|
|
## Example
|
|
|
|
iex> connect_hubs()
|
|
:ok
|
|
|
|
"""
|
|
@spec connect_hubs() :: :ok
|
|
def connect_hubs do
|
|
for hub <- get_hubs(), do: connect_hub(hub)
|
|
|
|
:ok
|
|
end
|
|
|
|
defp connect_hub(hub) do
|
|
if child_spec = Provider.connection_spec(hub) do
|
|
case DynamicSupervisor.start_child(@supervisor, child_spec) do
|
|
{:ok, _} ->
|
|
:ok
|
|
|
|
{:error, {:already_started, _pid}} ->
|
|
:ok
|
|
|
|
{:error, reason} ->
|
|
Logger.error("Could not start Workspace #{hub.id}: #{Exception.format_exit(reason)}")
|
|
end
|
|
end
|
|
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Gets a list of hub secrets.
|
|
|
|
It gets from all hubs with secret management.
|
|
"""
|
|
@spec get_secrets() :: list(Secret.t())
|
|
def get_secrets do
|
|
for hub <- get_hubs(),
|
|
secret <- Provider.get_secrets(hub),
|
|
do: secret
|
|
end
|
|
|
|
@doc """
|
|
Gets a list of secrets for given hub.
|
|
"""
|
|
@spec get_secrets(Provider.t()) :: list(Secret.t())
|
|
def get_secrets(hub) do
|
|
hub
|
|
|> Provider.get_secrets()
|
|
|> Enum.sort()
|
|
end
|
|
|
|
@doc """
|
|
Creates a secret for given hub.
|
|
"""
|
|
@spec create_secret(Provider.t(), Secret.t()) ::
|
|
:ok
|
|
| {:error, Ecto.Changeset.t()}
|
|
| {:transport_error, String.t()}
|
|
def create_secret(hub, %Secret{} = secret) do
|
|
Provider.create_secret(hub, secret)
|
|
end
|
|
|
|
@doc """
|
|
Updates a secret for given hub.
|
|
"""
|
|
@spec update_secret(Provider.t(), Secret.t()) ::
|
|
:ok
|
|
| {:error, Ecto.Changeset.t()}
|
|
| {:transport_error, String.t()}
|
|
def update_secret(hub, %Secret{} = secret) do
|
|
Provider.update_secret(hub, secret)
|
|
end
|
|
|
|
@doc """
|
|
Deletes a secret for given hub.
|
|
"""
|
|
@spec delete_secret(Provider.t(), Secret.t()) :: :ok | {:transport_error, String.t()}
|
|
def delete_secret(hub, %Secret{} = secret) do
|
|
Provider.delete_secret(hub, secret)
|
|
end
|
|
|
|
@doc """
|
|
Generates a notebook stamp.
|
|
"""
|
|
@spec notebook_stamp(Provider.t(), iodata(), map()) ::
|
|
{:ok, Provider.notebook_stamp()} | :skip | {:error, String.t()}
|
|
def notebook_stamp(hub, notebook_source, metadata) do
|
|
Provider.notebook_stamp(hub, notebook_source, metadata)
|
|
end
|
|
|
|
@doc """
|
|
Verifies a notebook stamp and returns the decrypted metadata.
|
|
"""
|
|
@spec verify_notebook_stamp(Provider.t(), iodata(), Provider.notebook_stamp()) ::
|
|
{:ok, metadata :: map()} | {:error, :invalid | :too_recent_version}
|
|
def verify_notebook_stamp(hub, notebook_source, stamp) do
|
|
Provider.verify_notebook_stamp(hub, notebook_source, stamp)
|
|
end
|
|
|
|
@doc """
|
|
Gets a list of file systems from all hubs.
|
|
"""
|
|
@spec get_file_systems() :: list(FileSystem.t())
|
|
def get_file_systems() do
|
|
file_systems = Enum.flat_map(get_hubs(), &Provider.get_file_systems/1)
|
|
local_file_system = Livebook.Config.local_file_system()
|
|
|
|
[local_file_system | Enum.sort_by(file_systems, & &1.id)]
|
|
end
|
|
|
|
@doc """
|
|
Gets a list of file systems for given hub.
|
|
"""
|
|
@spec get_file_systems(Provider.t(), keyword()) :: list(FileSystem.t())
|
|
def get_file_systems(hub, opts \\ []) do
|
|
hub_file_systems = Provider.get_file_systems(hub)
|
|
sorted_hub_file_systems = Enum.sort_by(hub_file_systems, & &1.id)
|
|
|
|
if opts[:hub_only],
|
|
do: sorted_hub_file_systems,
|
|
else: [Livebook.Config.local_file_system() | sorted_hub_file_systems]
|
|
end
|
|
|
|
@doc """
|
|
Creates a file system for given hub.
|
|
"""
|
|
@spec create_file_system(Provider.t(), FileSystem.t()) ::
|
|
:ok
|
|
| {:error, Ecto.Changeset.t()}
|
|
| {:transport_error, String.t()}
|
|
def create_file_system(hub, file_system) do
|
|
Provider.create_file_system(hub, file_system)
|
|
end
|
|
|
|
@doc """
|
|
Updates a file system for given hub.
|
|
"""
|
|
@spec update_file_system(Provider.t(), FileSystem.t()) ::
|
|
:ok
|
|
| {:error, Ecto.Changeset.t()}
|
|
| {:transport_error, String.t()}
|
|
def update_file_system(hub, file_system) do
|
|
Provider.update_file_system(hub, file_system)
|
|
end
|
|
|
|
@doc """
|
|
Deletes a file system for given hub.
|
|
"""
|
|
@spec delete_file_system(Provider.t(), FileSystem.t()) :: :ok | {:transport_error, String.t()}
|
|
def delete_file_system(hub, file_system) do
|
|
Provider.delete_file_system(hub, file_system)
|
|
end
|
|
|
|
@doc """
|
|
Gets a list of hub app specs.
|
|
"""
|
|
@spec get_app_specs() :: list(Livebook.AppSpec.t())
|
|
def get_app_specs() do
|
|
for hub <- get_hubs(),
|
|
app_spec <- Provider.get_app_specs(hub),
|
|
do: app_spec
|
|
end
|
|
end
|