Packages
livebook
0.16.0
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/hubs/team_client.ex
defmodule Livebook.Hubs.TeamClient do
use GenServer
require Logger
alias Livebook.Apps
alias Livebook.FileSystem
alias Livebook.FileSystems
alias Livebook.Hubs
alias Livebook.Secrets
alias Livebook.Teams
@registry Livebook.HubsRegistry
@supervisor Livebook.HubsSupervisor
defstruct [
:connection_pid,
:hub,
:connection_status,
:derived_key,
:deployment_group_id,
connected?: false,
secrets: [],
file_systems: [],
deployment_groups: [],
app_deployments: [],
agents: [],
app_deployment_statuses: nil
]
@type registry_name :: {:via, Registry, {Livebook.HubsRegistry, String.t()}}
@doc """
Connects the Team client with WebSocket server.
"""
@spec start_link(Hubs.Team.t()) :: GenServer.on_start()
def start_link(%Hubs.Team{} = team) do
GenServer.start_link(__MODULE__, team, name: registry_name(team.id))
end
@doc """
Returns the client pid for the given hub id.
"""
@spec get_pid(String.t()) :: pid() | nil
def get_pid(id) do
GenServer.whereis(registry_name(id))
end
@doc """
Stops the WebSocket server.
"""
@spec stop(String.t()) :: :ok
def stop(id) do
if pid = GenServer.whereis(registry_name(id)) do
DynamicSupervisor.terminate_child(@supervisor, pid)
end
:ok
end
@doc """
Returns a list of cached secrets.
"""
@spec get_secrets(String.t()) :: list(Secrets.Secret.t())
def get_secrets(id) do
GenServer.call(registry_name(id), :get_secrets)
end
@doc """
Returns a list of cached file systems.
"""
@spec get_file_systems(String.t()) :: list(FileSystem.t())
def get_file_systems(id) do
GenServer.call(registry_name(id), :get_file_systems)
end
@doc """
Returns the latest status from connection.
"""
@spec get_connection_status(String.t()) :: String.t() | nil
def get_connection_status(id) do
GenServer.call(registry_name(id), :get_connection_status)
catch
:exit, _ -> "connection refused"
end
@doc """
Returns a list of cached deployment groups.
"""
@spec get_deployment_groups(String.t()) :: list(Teams.DeploymentGroup.t())
def get_deployment_groups(id) do
GenServer.call(registry_name(id), :get_deployment_groups)
end
@doc """
Returns a list of cached app deployments.
"""
@spec get_app_deployments(String.t()) :: list(Teams.AppDeployment.t())
def get_app_deployments(id) do
GenServer.call(registry_name(id), :get_app_deployments)
end
@doc """
Returns a list of cached app deployments that should be deployed on
this instance.
"""
@spec get_agent_app_deployments(String.t()) :: list(Teams.AppDeployment.t())
def get_agent_app_deployments(id) do
GenServer.call(registry_name(id), :get_agent_app_deployments)
end
@doc """
Returns information necessary to download and decrypt archive for
app deployment with the given id.
"""
@spec get_app_deployment_download_info(String.t(), String.t()) ::
{:ok, Teams.AppDeployment.t(), derived_key :: binary()} | :error
def get_app_deployment_download_info(id, app_deployment_id) do
GenServer.call(registry_name(id), {:get_app_deployment_download_info, app_deployment_id})
end
@doc """
Returns a list of cached agents.
"""
@spec get_agents(String.t()) :: list(Teams.Agent.t())
def get_agents(id) do
GenServer.call(registry_name(id), :get_agents)
end
@doc """
Returns if the Team client uses Livebook Teams identity provider.
"""
@spec identity_enabled?(String.t()) :: boolean()
def identity_enabled?(id) do
GenServer.call(registry_name(id), :identity_enabled?)
end
@doc """
Returns a list of cached environment variables.
"""
@spec get_environment_variables(String.t()) :: list(Teams.Agent.t())
def get_environment_variables(id) do
GenServer.call(registry_name(id), :get_environment_variables)
end
@doc """
Returns if the given user groups has full access to app server.
"""
@spec user_full_access?(String.t(), list(map())) :: boolean()
def user_full_access?(id, groups) do
GenServer.call(registry_name(id), {:check_full_access, groups})
end
@doc """
Returns if the given user groups has access to given app.
"""
@spec user_app_access?(String.t(), list(map()), String.t()) :: boolean()
def user_app_access?(id, groups, slug) do
GenServer.call(registry_name(id), {:check_app_access, groups, slug})
end
@doc """
Returns if the Team client is connected.
"""
@spec connected?(String.t()) :: boolean()
def connected?(id) do
GenServer.call(registry_name(id), :connected?)
catch
:exit, _ -> false
end
@doc """
Enqueues the synchronous event to be handled by the Team client.
"""
@spec handle_event(String.t(), {atom(), LivebookProto.event_proto()}) :: :ok
def handle_event(id, {topic, data}) do
GenServer.cast(registry_name(id), {:event, topic, data})
end
## GenServer callbacks
@impl true
def init(%Hubs.Team{offline: nil} = team) do
Apps.Manager.subscribe()
derived_key = Teams.derive_key(team.teams_key)
headers =
if team.user_id do
[
{"x-lb-version", Livebook.Config.app_version()},
{"x-user", to_string(team.user_id)},
{"x-org", to_string(team.org_id)},
{"x-org-key", to_string(team.org_key_id)},
{"x-session-token", team.session_token}
]
else
[
{"x-lb-version", Livebook.Config.app_version()},
{"x-org", to_string(team.org_id)},
{"x-org-key", to_string(team.org_key_id)},
{"x-agent-name", Livebook.Config.agent_name()},
{"x-agent-key", team.session_token}
]
end
{:ok, pid} = Teams.Connection.start_link(self(), headers)
{:ok, %__MODULE__{connection_pid: pid, hub: team, derived_key: derived_key}}
end
def init(%Hubs.Team{} = team) do
derived_key = Teams.derive_key(team.teams_key)
{:ok,
%__MODULE__{
hub: team,
secrets: team.offline.secrets,
file_systems: team.offline.file_systems,
derived_key: derived_key
}}
end
@impl true
def handle_call(:get_connection_status, _caller, state) do
{:reply, state.connection_status, state}
end
def handle_call(:connected?, _caller, state) do
{:reply, state.connected?, state}
end
def handle_call(:get_secrets, _caller, %{deployment_group_id: nil} = state) do
{:reply, state.secrets, state}
end
def handle_call(:get_secrets, _caller, state) do
case find_deployment_group(state.deployment_group_id, state.deployment_groups) do
nil ->
{:reply, state.secrets, state}
%{secrets: agent_secrets} ->
{:reply, Enum.uniq_by(agent_secrets ++ state.secrets, & &1.name), state}
end
end
def handle_call(:get_file_systems, _caller, state) do
{:reply, state.file_systems, state}
end
def handle_call(:get_deployment_groups, _caller, state) do
{:reply, state.deployment_groups, state}
end
def handle_call(:get_app_deployments, _caller, state) do
{:reply, state.app_deployments, state}
end
def handle_call(:get_agent_app_deployments, _caller, state) do
if state.deployment_group_id do
app_deployments =
Enum.filter(state.app_deployments, &(&1.deployment_group_id == state.deployment_group_id))
{:reply, app_deployments, state}
else
{:reply, [], state}
end
end
def handle_call({:get_app_deployment_download_info, app_deployment_id}, _caller, state) do
reply =
if app_deployment = Enum.find(state.app_deployments, &(&1.id == app_deployment_id)) do
{:ok, app_deployment, state.derived_key}
else
:error
end
{:reply, reply, state}
end
def handle_call(:get_agents, _caller, state) do
{:reply, state.agents, state}
end
def handle_call(:get_environment_variables, _caller, state) do
environment_variables = Enum.flat_map(state.deployment_groups, & &1.environment_variables)
{:reply, environment_variables, state}
end
def handle_call(:identity_enabled?, _caller, %{deployment_group_id: nil} = state) do
{:reply, false, state}
end
def handle_call(:identity_enabled?, _caller, %{deployment_group_id: id} = state) do
case fetch_deployment_group(id, state) do
{:ok, deployment_group} -> {:reply, deployment_group.teams_auth, state}
_ -> {:reply, false, state}
end
end
def handle_call({:check_full_access, groups}, _caller, state) do
if id = state.deployment_group_id do
case fetch_deployment_group(id, state) do
{:ok, deployment_group} ->
{:reply,
not deployment_group.teams_auth or
not deployment_group.groups_auth or
authorized_group?(deployment_group.authorization_groups, groups), state}
_ ->
{:reply, false, state}
end
else
{:reply, true, state}
end
end
def handle_call({:check_app_access, groups, slug}, _caller, state) do
if id = state.deployment_group_id do
with {:ok, deployment_group} <- fetch_deployment_group(id, state),
{:ok, app_deployment} <- fetch_app_deployment_from_slug(slug, state) do
app_access? =
not deployment_group.teams_auth or
not deployment_group.groups_auth or
(authorized_group?(deployment_group.authorization_groups, groups) or
authorized_group?(app_deployment.authorization_groups, groups))
{:reply, app_access?, state}
else
_ -> {:reply, false, state}
end
else
{:reply, true, state}
end
end
@impl true
def handle_info(:connected, state) do
Hubs.Broadcasts.hub_connected(state.hub.id)
{:noreply, %{state | connected?: true, connection_status: nil}}
end
def handle_info({:connection_error, reason}, state) do
Hubs.Broadcasts.hub_connection_failed(state.hub.id, reason)
{:noreply, %{state | connected?: false, connection_status: reason}}
end
def handle_info({:server_error, reason}, state) do
Hubs.Broadcasts.hub_server_error(state.hub.id, "#{state.hub.hub_name}: #{reason}")
:ok = Hubs.delete_hub(state.hub.id)
{:noreply, %{state | connected?: false}}
end
def handle_info({:event, topic, data}, state) do
Logger.debug("Received event #{topic} with data: #{inspect(data)}")
{:noreply, handle_event(topic, data, state)}
end
def handle_info({:apps_manager_status, _}, state)
when not state.connected? or state.deployment_group_id == nil do
{:noreply, state}
end
def handle_info({:apps_manager_status, entries}, %{hub: %{id: id}} = state) do
app_deployment_statuses =
for %{app_spec: %Apps.TeamsAppSpec{hub_id: ^id} = app_spec, running?: running?} <- entries do
status = if running?, do: :available, else: :preparing
%LivebookProto.AppDeploymentStatus{
id: app_spec.app_deployment_id,
deployment_group_id: state.deployment_group_id,
version: app_spec.version,
status: status
}
end
# The manager can send the status list even if it didn't change,
# or it changed for non-teams app spec, so we check to send the
# event only when necessary
if app_deployment_statuses == state.app_deployment_statuses do
{:noreply, state}
else
report = %LivebookProto.AppDeploymentStatusReport{
app_deployment_statuses: app_deployment_statuses
}
Logger.debug("Sending apps manager report to Teams server #{inspect(report)}")
message = LivebookProto.AppDeploymentStatusReport.encode(report)
:ok = Teams.Connection.send_message(state.connection_pid, message)
{:noreply, %{state | app_deployment_statuses: app_deployment_statuses}}
end
end
@impl true
def handle_cast({:event, topic, data}, state) do
Logger.debug("Received event #{topic} with data: #{inspect(data)}")
{:noreply, handle_event(topic, data, state)}
end
# Private
defp registry_name(id) do
{:via, Registry, {@registry, id}}
end
defp put_secret(state, secret) do
state = remove_secret(state, secret)
%{state | secrets: [secret | state.secrets]}
end
defp remove_secret(state, secret) do
%{state | secrets: Enum.reject(state.secrets, &(&1.name == secret.name))}
end
defp build_secret(state, %{name: name, value: value} = attrs) do
{:ok, decrypted_value} = Teams.decrypt(value, state.derived_key)
%Secrets.Secret{
name: name,
value: decrypted_value,
hub_id: state.hub.id,
deployment_group_id: Map.get(attrs, :deployment_group_id)
}
end
defp put_file_system(state, file_system) do
state = remove_file_system(state, file_system)
%{state | file_systems: [file_system | state.file_systems]}
end
defp remove_file_system(state, file_system) do
%{
state
| file_systems:
Enum.reject(state.file_systems, &(&1.external_id == file_system.external_id))
}
end
defp build_file_system(state, file_system) do
{:ok, decrypted_value} = Teams.decrypt(file_system.value, state.derived_key)
dumped_data =
decrypted_value
|> JSON.decode!()
|> Map.put("external_id", file_system.id)
FileSystems.load(file_system.type, dumped_data)
end
defp put_deployment_group(state, deployment_group) do
state = remove_deployment_group(state, deployment_group)
%{state | deployment_groups: [deployment_group | state.deployment_groups]}
end
defp remove_deployment_group(state, deployment_group) do
%{
state
| deployment_groups: Enum.reject(state.deployment_groups, &(&1.id == deployment_group.id))
}
end
defp put_app_deployment(state, app_deployment) do
state = remove_app_deployment(state, app_deployment)
app_deployments = [app_deployment | state.app_deployments]
%{state | app_deployments: Enum.sort_by(app_deployments, & &1.slug)}
end
defp remove_app_deployment(state, app_deployment) do
%{
state
| app_deployments: Enum.reject(state.app_deployments, &(&1.id == app_deployment.id))
}
end
defp build_agent_key(agent_key) do
%Teams.AgentKey{
id: agent_key.id,
key: agent_key.key,
deployment_group_id: agent_key.deployment_group_id
}
end
defp build_deployment_group(state, %LivebookProto.DeploymentGroup{} = deployment_group) do
secrets = Enum.map(deployment_group.secrets, &build_secret(state, &1))
agent_keys = Enum.map(deployment_group.agent_keys, &build_agent_key/1)
environment_variables = build_environment_variables(state, deployment_group)
authorization_groups = build_authorization_groups(deployment_group)
%Teams.DeploymentGroup{
id: deployment_group.id,
name: deployment_group.name,
mode: atomize(deployment_group.mode),
hub_id: state.hub.id,
secrets: secrets,
agent_keys: agent_keys,
environment_variables: environment_variables,
clustering: nullify(deployment_group.clustering),
url: nullify(deployment_group.url),
teams_auth: deployment_group.teams_auth,
groups_auth: deployment_group.groups_auth,
authorization_groups: authorization_groups
}
end
defp build_deployment_group(state, %{mode: _} = deployment_group_created) do
agent_keys = Enum.map(deployment_group_created.agent_keys, &build_agent_key/1)
%Teams.DeploymentGroup{
id: deployment_group_created.id,
name: deployment_group_created.name,
mode: atomize(deployment_group_created.mode),
hub_id: state.hub.id,
secrets: [],
agent_keys: agent_keys,
environment_variables: [],
clustering: nullify(deployment_group_created.clustering),
url: nullify(deployment_group_created.url),
teams_auth: deployment_group_created.teams_auth,
authorization_groups: []
}
end
defp build_deployment_group(state, deployment_group_updated) do
secrets = Enum.map(deployment_group_updated.secrets, &build_secret(state, &1))
agent_keys = Enum.map(deployment_group_updated.agent_keys, &build_agent_key/1)
environment_variables = build_environment_variables(state, deployment_group_updated)
authorization_groups = build_authorization_groups(deployment_group_updated)
{:ok, deployment_group} = fetch_deployment_group(deployment_group_updated.id, state)
%{
deployment_group
| name: deployment_group_updated.name,
secrets: secrets,
agent_keys: agent_keys,
environment_variables: environment_variables,
clustering: atomize(deployment_group_updated.clustering),
url: nullify(deployment_group_updated.url),
teams_auth: deployment_group_updated.teams_auth,
groups_auth: deployment_group_updated.groups_auth,
authorization_groups: authorization_groups
}
end
defp build_app_deployment(state, %LivebookProto.AppDeployment{} = app_deployment) do
authorization_groups = build_authorization_groups(app_deployment)
%Teams.AppDeployment{
id: app_deployment.id,
slug: app_deployment.slug,
version: app_deployment.version,
sha: app_deployment.sha,
title: app_deployment.title,
multi_session: app_deployment.multi_session,
access_type: String.to_atom(app_deployment.access_type),
hub_id: state.hub.id,
deployment_group_id: app_deployment.deployment_group_id,
file: nil,
deployed_by: app_deployment.deployed_by,
deployed_at: DateTime.from_gregorian_seconds(app_deployment.deployed_at),
authorization_groups: authorization_groups
}
end
defp build_environment_variables(state, deployment_group_updated) do
for environment_variable <- deployment_group_updated.environment_variables do
%Teams.EnvironmentVariable{
name: environment_variable.name,
value: environment_variable.value,
hub_id: state.hub.id,
deployment_group_id: deployment_group_updated.id
}
end
end
defp build_authorization_groups(%{authorization_groups: authorization_groups}) do
for authorization_group <- authorization_groups do
%Teams.AuthorizationGroup{
provider_id: authorization_group.provider_id,
group_name: authorization_group.group_name
}
end
end
defp put_agent(state, agent) do
state = remove_agent(state, agent)
%{state | agents: [agent | state.agents]}
end
defp remove_agent(state, agent) do
%{state | agents: Enum.reject(state.agents, &(&1.id == agent.id))}
end
defp build_agent(state, %LivebookProto.Agent{} = agent) do
%Livebook.Teams.Agent{
id: agent.id,
name: agent.name,
hub_id: state.hub.id,
org_id: agent.org_id,
deployment_group_id: agent.deployment_group_id
}
end
defp handle_event(:secret_created, %Secrets.Secret{} = secret, state) do
Hubs.Broadcasts.secret_created(secret)
put_secret(state, secret)
end
defp handle_event(:secret_created, secret_created, state) do
handle_event(:secret_created, build_secret(state, secret_created), state)
end
defp handle_event(:secret_updated, %Secrets.Secret{} = secret, state) do
Hubs.Broadcasts.secret_updated(secret)
put_secret(state, secret)
end
defp handle_event(:secret_updated, secret_updated, state) do
handle_event(:secret_updated, build_secret(state, secret_updated), state)
end
defp handle_event(:secret_deleted, %Secrets.Secret{} = secret, state) do
Hubs.Broadcasts.secret_deleted(secret)
remove_secret(state, secret)
end
defp handle_event(:secret_deleted, %{name: name}, state) do
with {:ok, secret} <- fetch_secret(name, state) do
handle_event(:secret_deleted, secret, state)
end
end
defp handle_event(:file_system_created, %{external_id: _} = file_system, state) do
Hubs.Broadcasts.file_system_created(file_system)
put_file_system(state, file_system)
end
defp handle_event(:file_system_created, file_system_created, state) do
handle_event(:file_system_created, build_file_system(state, file_system_created), state)
end
defp handle_event(:file_system_updated, %{external_id: _} = file_system, state) do
Hubs.Broadcasts.file_system_updated(file_system)
put_file_system(state, file_system)
end
defp handle_event(:file_system_updated, file_system_updated, state) do
handle_event(:file_system_updated, build_file_system(state, file_system_updated), state)
end
defp handle_event(:file_system_deleted, %{external_id: _} = file_system, state) do
Hubs.Broadcasts.file_system_deleted(file_system)
remove_file_system(state, file_system)
end
defp handle_event(:file_system_deleted, %{id: external_id}, state) do
with {:ok, file_system} <- fetch_file_system(external_id, state) do
handle_event(:file_system_deleted, file_system, state)
end
end
defp handle_event(:deployment_group_created, %Teams.DeploymentGroup{} = deployment_group, state) do
Teams.Broadcasts.deployment_group_created(deployment_group)
put_deployment_group(state, deployment_group)
end
defp handle_event(:deployment_group_created, deployment_group_created, state) do
handle_event(
:deployment_group_created,
build_deployment_group(state, deployment_group_created),
state
)
end
defp handle_event(:deployment_group_updated, %Teams.DeploymentGroup{} = deployment_group, state) do
Teams.Broadcasts.deployment_group_updated(deployment_group)
with {:ok, current_deployment_group} <- fetch_deployment_group(deployment_group.id, state) do
if state.deployment_group_id == deployment_group.id and
(current_deployment_group.authorization_groups != deployment_group.authorization_groups or
current_deployment_group.groups_auth != deployment_group.groups_auth or
current_deployment_group.teams_auth != deployment_group.teams_auth) do
Teams.Broadcasts.server_authorization_updated(deployment_group)
end
end
put_deployment_group(state, deployment_group)
end
defp handle_event(:deployment_group_updated, deployment_group_updated, state) do
handle_event(
:deployment_group_updated,
build_deployment_group(state, deployment_group_updated),
state
)
end
defp handle_event(:deployment_group_deleted, %Teams.DeploymentGroup{} = deployment_group, state) do
Teams.Broadcasts.deployment_group_deleted(deployment_group)
remove_deployment_group(state, deployment_group)
end
defp handle_event(:deployment_group_deleted, %{id: id}, state) do
with {:ok, deployment_group} <- fetch_deployment_group(id, state) do
handle_event(:deployment_group_deleted, deployment_group, state)
end
end
defp handle_event(:user_connected, user_connected, state) do
state
|> update_hub(user_connected)
|> dispatch_secrets(user_connected)
|> dispatch_file_systems(user_connected)
|> dispatch_deployment_groups(user_connected)
|> dispatch_app_deployments(user_connected)
|> dispatch_agents(user_connected)
|> dispatch_connection()
end
defp handle_event(:agent_connected, agent_connected, state) do
%{state | deployment_group_id: to_string(agent_connected.deployment_group_id)}
|> update_hub(agent_connected)
|> dispatch_secrets(agent_connected)
|> dispatch_file_systems(agent_connected)
|> dispatch_deployment_groups(agent_connected)
|> dispatch_app_deployments(agent_connected)
|> dispatch_agents(agent_connected)
|> dispatch_connection()
end
defp handle_event(:app_deployment_started, %Teams.AppDeployment{} = app_deployment, state) do
manager_sync(app_deployment, state)
Teams.Broadcasts.app_deployment_started(app_deployment)
put_app_deployment(state, app_deployment)
end
defp handle_event(:app_deployment_started, app_deployment_started, state) do
handle_event(
:app_deployment_started,
build_app_deployment(state, app_deployment_started.app_deployment),
state
)
end
defp handle_event(:app_deployment_stopped, %Teams.AppDeployment{} = app_deployment, state) do
manager_sync(app_deployment, state)
Teams.Broadcasts.app_deployment_stopped(app_deployment)
remove_app_deployment(state, app_deployment)
end
defp handle_event(:app_deployment_stopped, %{id: id}, state) do
with {:ok, app_deployment} <- fetch_app_deployment(id, state) do
handle_event(:app_deployment_stopped, app_deployment, state)
end
end
defp handle_event(:app_deployment_updated, %Teams.AppDeployment{} = app_deployment, state) do
manager_sync(app_deployment, state)
Teams.Broadcasts.app_deployment_updated(app_deployment)
put_app_deployment(state, app_deployment)
end
defp handle_event(:app_deployment_updated, app_deployment_updated, state) do
handle_event(
:app_deployment_updated,
build_app_deployment(state, app_deployment_updated.app_deployment),
state
)
end
defp handle_event(:agent_joined, %Teams.Agent{} = agent, state) do
Teams.Broadcasts.agent_joined(agent)
put_agent(state, agent)
end
defp handle_event(:agent_joined, agent_joined, state) do
handle_event(:agent_joined, build_agent(state, agent_joined.agent), state)
end
defp handle_event(:agent_left, %Teams.Agent{} = agent, state) do
Teams.Broadcasts.agent_left(agent)
remove_agent(state, agent)
end
defp handle_event(:agent_left, %{id: id}, state) do
with {:ok, agent} <- fetch_agent(id, state) do
handle_event(:agent_left, agent, state)
end
end
defp handle_event(:user_deleted, %{id: id}, state) do
if id == to_string(state.hub.user_id) do
send(self(), {:server_error, "you were removed from the org"})
end
state
end
defp handle_event(:org_updated, org_updated, state) do
update_hub(state, org_updated)
end
defp dispatch_secrets(state, %{secrets: secrets}) do
decrypted_secrets = Enum.map(secrets, &build_secret(state, &1))
{created, deleted, updated} =
diff(
state.secrets,
decrypted_secrets,
&(&1.name == &2.name and &1.value == &2.value),
&(&1.name == &2.name),
&(&1.name == &2.name and &1.value != &2.value)
)
dispatch_events(state,
secret_deleted: deleted,
secret_created: created,
secret_updated: updated
)
end
defp dispatch_file_systems(state, %{file_systems: file_systems}) do
decrypted_file_systems = Enum.map(file_systems, &build_file_system(state, &1))
{created, deleted, updated} =
diff(
state.file_systems,
decrypted_file_systems,
&(&1.external_id == &2.external_id)
)
dispatch_events(state,
file_system_deleted: deleted,
file_system_created: created,
file_system_updated: updated
)
end
defp dispatch_deployment_groups(state, %{deployment_groups: deployment_groups}) do
deployment_groups = Enum.map(deployment_groups, &build_deployment_group(state, &1))
{created, deleted, updated} =
diff(state.deployment_groups, deployment_groups, &(&1.id == &2.id))
dispatch_events(state,
deployment_group_deleted: deleted,
deployment_group_created: created,
deployment_group_updated: updated
)
end
defp dispatch_app_deployments(state, %{app_deployments: app_deployments}) do
app_deployments = Enum.map(app_deployments, &build_app_deployment(state, &1))
{started, stopped, _} = diff(state.app_deployments, app_deployments, &(&1.id == &2.id))
dispatch_events(state, app_deployment_started: started, app_deployment_stopped: stopped)
end
defp dispatch_agents(state, %{agents: agents}) do
agents = Enum.map(agents, &build_agent(state, &1))
{joined, left, _} = diff(state.agents, agents, &(&1.id == &2.id))
dispatch_events(state, agent_joined: joined, agent_left: left)
end
defp dispatch_connection(%{hub: %{id: id}} = state) do
Teams.Broadcasts.client_connected(id)
state
end
defp update_hub(state, %LivebookProto.UserConnected{billing_status: billing_status}) do
update_hub(
state,
&put_billing_status(&1, billing_status)
)
end
defp update_hub(state, %LivebookProto.AgentConnected{
public_key: org_public_key,
billing_status: billing_status
}) do
update_hub(
state,
&(&1
|> struct!(org_public_key: org_public_key)
|> put_billing_status(billing_status))
)
end
defp update_hub(state, %LivebookProto.OrgUpdated{billing_status: billing_status}) do
update_hub(
state,
&put_billing_status(&1, billing_status)
)
end
defp update_hub(state, fun) when is_function(fun, 1) do
hub = fun.(state.hub)
if Hubs.hub_exists?(hub.id) do
Hubs.save_hub(hub)
end
put_in(state.hub, hub)
end
# TODO: Remove when Billing is public
defp put_billing_status(hub, nil = _status) do
put_in(
hub.billing_status,
%{disabled: false, type: nil}
)
end
defp put_billing_status(hub, %LivebookProto.BillingStatus{} = status) do
put_in(
hub.billing_status,
Map.merge(%{disabled: status.disabled}, put_billing_status(status.type))
)
end
defp put_billing_status(
{:trialing,
%LivebookProto.BillingStatusTrialing{
trial_ends_at: trial_ends_at
}}
) do
%{type: :trialing, trial_ends_at: DateTime.from_unix!(trial_ends_at)}
end
defp put_billing_status(
{:trial_ended,
%LivebookProto.BillingStatusTrialEnded{
trial_ends_at: trial_ends_at
}}
) do
%{type: :trial_ended, trial_ends_at: DateTime.from_unix!(trial_ends_at)}
end
defp put_billing_status(
{:canceling,
%LivebookProto.BillingStatusCanceling{
cancel_at: cancel_at
}}
) do
%{type: :canceling, cancel_at: DateTime.from_unix!(cancel_at)}
end
defp put_billing_status({:canceled, %LivebookProto.BillingStatusCanceled{}}) do
%{type: :canceled}
end
defp put_billing_status(_other) do
%{type: nil}
end
defp diff(old_list, new_list, fun, deleted_fun \\ nil, updated_fun \\ nil) do
deleted_fun = unless deleted_fun, do: fun, else: deleted_fun
updated_fun = unless updated_fun, do: fun, else: updated_fun
created = Enum.reject(new_list, fn item -> Enum.find(old_list, &fun.(&1, item)) end)
deleted = Enum.reject(old_list, fn item -> Enum.find(new_list, &deleted_fun.(&1, item)) end)
updated = Enum.filter(new_list, fn item -> Enum.find(old_list, &updated_fun.(&1, item)) end)
{created, deleted, updated}
end
defp dispatch_events(state, events_by_topic) do
for {topic, events} <- events_by_topic,
event <- events,
reduce: state,
do: (acc -> handle_event(topic, event, acc))
end
defp find_deployment_group(nil, _), do: nil
defp find_deployment_group(id, groups), do: Enum.find(groups, &(&1.id == id))
defp fetch_deployment_group(id, state),
do: fetch_entry(state.deployment_groups, &(&1.id == id), state)
defp fetch_secret(name, state), do: fetch_entry(state.secrets, &(&1.name == name), state)
defp fetch_file_system(external_id, state),
do: fetch_entry(state.file_systems, &(&1.external_id == external_id), state)
defp fetch_agent(id, state), do: fetch_entry(state.agents, &(&1.id == id), state)
defp fetch_app_deployment(id, state),
do: fetch_entry(state.app_deployments, &(&1.id == id), state)
defp fetch_app_deployment_from_slug(slug, state),
do: fetch_entry(state.app_deployments, &(&1.slug == slug), state)
defp fetch_entry(entries, fun, state) do
if entry = Enum.find(entries, fun) do
{:ok, entry}
else
state
end
end
# We cannot use to_existing_atom because the atoms
# may not have been loaded. Luckily, we can trust
# on Livebook Teams as a source.
defp atomize(value) when value in [nil, ""], do: nil
defp atomize(value), do: String.to_atom(value)
defp nullify(""), do: nil
defp nullify(value), do: value
defp manager_sync(app_deployment, state) do
# We only need to sync if the app deployment belongs to the current
# deployment group
if app_deployment.deployment_group_id == state.deployment_group_id do
# Each node runs the teams client, but we only need to call sync once
if Apps.Manager.local?() do
Apps.Manager.sync_permanent_apps()
end
end
end
defp authorized_group?(authorization_groups, groups) do
Enum.any?(authorization_groups, fn %{provider_id: id, group_name: name} ->
%{"provider_id" => id, "group_name" => name} in groups
end)
end
end