Packages
livebook
0.17.2
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/teams/requests.ex
defmodule Livebook.Teams.Requests do
alias Livebook.FileSystem
alias Livebook.FileSystems
alias Livebook.Hubs.Team
alias Livebook.Secrets.Secret
alias Livebook.Teams
@org_token_prefix Teams.Constants.org_token_prefix()
@error_message "Something went wrong, try again later or please file a bug if it persists"
@unauthorized_error_message "You are not authorized to perform this action, make sure you have the access and you are not in a Livebook App Server/Offline instance"
@unauthorized_app_deployment_error_message "Deployment not authorized, check deploy permissions for this deployment group"
@typep api_result :: {:ok, map()} | error_result()
@typep error_result :: {:error, map() | String.t()} | {:transport_error, String.t()}
@doc false
def error_message(), do: @error_message
@doc """
Send a request to Livebook Team API to create a new org.
"""
@spec create_org(Teams.Org.t()) :: api_result()
def create_org(org) do
post("/api/v1/org-request", %{name: org.name, key_hash: Teams.Org.key_hash(org)})
end
@doc """
Send a request to Livebook Team API to join an org.
"""
@spec join_org(Teams.Org.t()) :: api_result()
def join_org(org) do
post("/api/v1/org-request/join", %{name: org.name, key_hash: Teams.Org.key_hash(org)})
end
@doc """
Send a request to Livebook Team API to get an org request.
"""
@spec get_org_request_completion_data(pos_integer(), binary) :: api_result()
def get_org_request_completion_data(id, device_code) do
get("/api/v1/org-request/#{id}?device_code=#{device_code}")
end
@doc """
Send a request to Livebook Team API to sign the given payload.
"""
@spec org_sign(Team.t(), String.t()) :: api_result()
def org_sign(team, payload) do
post("/api/v1/org/sign", %{payload: payload}, team)
end
@doc """
Send a request to Livebook Team API to create a secret.
"""
@spec create_secret(Team.t(), Secret.t()) :: api_result()
def create_secret(team, %{deployment_group_id: nil} = secret) do
secret_key = Teams.derive_key(team.teams_key)
secret_value = Teams.encrypt(secret.value, secret_key)
post("/api/v1/org/secrets", %{name: secret.name, value: secret_value}, team)
end
def create_secret(team, secret) do
secret_key = Teams.derive_key(team.teams_key)
secret_value = Teams.encrypt(secret.value, secret_key)
params = %{
name: secret.name,
value: secret_value,
deployment_group_id: secret.deployment_group_id
}
post("/api/v1/org/deployment-groups/secrets", params, team)
end
@doc """
Send a request to Livebook Team API to update a secret.
"""
@spec update_secret(Team.t(), Secret.t()) :: api_result()
def update_secret(team, %{deployment_group_id: nil} = secret) do
secret_key = Teams.derive_key(team.teams_key)
secret_value = Teams.encrypt(secret.value, secret_key)
put("/api/v1/org/secrets", %{name: secret.name, value: secret_value}, team)
end
def update_secret(team, secret) do
secret_key = Teams.derive_key(team.teams_key)
secret_value = Teams.encrypt(secret.value, secret_key)
params = %{
name: secret.name,
value: secret_value,
deployment_group_id: secret.deployment_group_id
}
put("/api/v1/org/deployment-groups/secrets", params, team)
end
@doc """
Send a request to Livebook Team API to delete a secret.
"""
@spec delete_secret(Team.t(), Secret.t()) :: api_result()
def delete_secret(team, %{deployment_group_id: nil} = secret) do
delete("/api/v1/org/secrets", %{name: secret.name}, team)
end
def delete_secret(team, secret) do
params = %{name: secret.name, deployment_group_id: secret.deployment_group_id}
delete("/api/v1/org/deployment-groups/secrets", params, team)
end
@doc """
Send a request to Livebook Team API to create a file system.
"""
@spec create_file_system(Team.t(), FileSystem.t()) :: api_result()
def create_file_system(team, file_system) do
secret_key = Teams.derive_key(team.teams_key)
type = FileSystems.type(file_system)
%{name: name} = FileSystem.external_metadata(file_system)
attrs = FileSystem.dump(file_system)
json = JSON.encode!(attrs)
params = %{
name: name,
type: to_string(type),
value: Teams.encrypt(json, secret_key)
}
post("/api/v1/org/file-systems", params, team)
end
@doc """
Send a request to Livebook Team API to update a file system.
"""
@spec update_file_system(Team.t(), FileSystem.t()) :: api_result()
def update_file_system(team, file_system) do
secret_key = Teams.derive_key(team.teams_key)
type = FileSystems.type(file_system)
%{name: name} = FileSystem.external_metadata(file_system)
attrs = FileSystem.dump(file_system)
json = JSON.encode!(attrs)
params = %{
id: file_system.external_id,
name: name,
type: to_string(type),
value: Teams.encrypt(json, secret_key)
}
put("/api/v1/org/file-systems", params, team)
end
@doc """
Send a request to Livebook Team API to delete a file system.
"""
@spec delete_file_system(Team.t(), FileSystem.t()) :: api_result()
def delete_file_system(team, file_system) do
delete("/api/v1/org/file-systems", %{id: file_system.external_id}, team)
end
@doc """
Send a request to Livebook Team API to create a deployment group.
"""
@spec create_deployment_group(Team.t(), Teams.DeploymentGroup.t()) :: api_result()
def create_deployment_group(team, deployment_group) do
params = %{
name: deployment_group.name,
mode: deployment_group.mode,
clustering: deployment_group.clustering,
url: deployment_group.url
}
post("/api/v1/org/deployment-groups", params, team)
end
@doc """
Send a request to Livebook Team API to deploy an app.
"""
@spec deploy_app(Team.t(), Teams.AppDeployment.t()) :: api_result()
def deploy_app(team, app_deployment) do
secret_key = Teams.derive_key(team.teams_key)
params = %{
title: app_deployment.title,
slug: app_deployment.slug,
multi_session: app_deployment.multi_session,
access_type: app_deployment.access_type,
deployment_group_id: app_deployment.deployment_group_id,
sha: app_deployment.sha
}
encrypted_content = Teams.encrypt(app_deployment.file, secret_key)
upload("/api/v1/org/apps", encrypted_content, params, team)
end
@doc """
Send a request to Livebook Team API to download an app revision.
"""
@spec download_revision(Team.t(), Teams.AppDeployment.t()) :: {:ok, binary()} | error_result()
def download_revision(team, app_deployment) do
params = %{id: app_deployment.id, deployment_group_id: app_deployment.deployment_group_id}
get("/api/v1/org/apps", params, team)
end
@doc """
Send a request to Livebook Team API to create a new auth request.
"""
@spec create_auth_request(Team.t()) :: api_result()
def create_auth_request(team) do
post("/api/v1/org/identity", %{}, team)
end
@doc """
Send a request to Livebook Team API to get the access token from given auth request code.
"""
@spec retrieve_access_token(Team.t(), String.t()) :: api_result()
def retrieve_access_token(team, code) do
post("/api/v1/org/identity/token", %{code: code}, team)
end
@doc """
Send a request to Livebook Team API to get the user information from given access token.
"""
@spec get_user_info(Team.t(), String.t()) :: api_result()
def get_user_info(team, access_token) do
get("/api/v1/org/identity", %{access_token: access_token}, team)
end
@doc """
Send a request to Livebook Team API to return a session using an org token.
"""
@spec fetch_cli_session(map()) :: api_result()
def fetch_cli_session(config) do
post("/api/v1/cli/auth", %{}, config)
end
@doc """
Send a request to Livebook Team API to deploy an app using an org token.
"""
@spec deploy_app_from_cli(Team.t(), Teams.AppDeployment.t(), integer()) :: api_result()
def deploy_app_from_cli(team, app_deployment, deployment_group_id) do
secret_key = Teams.derive_key(team.teams_key)
params = %{
title: app_deployment.title,
slug: app_deployment.slug,
multi_session: app_deployment.multi_session,
access_type: app_deployment.access_type,
deployment_group_id: deployment_group_id,
sha: app_deployment.sha
}
encrypted_content = Teams.encrypt(app_deployment.file, secret_key)
upload("/api/v1/cli/org/apps", encrypted_content, params, team)
end
@doc """
Normalizes errors map into errors for the given schema.
"""
@spec to_error_list(module(), %{String.t() => list(String.t())}) ::
list({atom(), list(String.t())})
def to_error_list(struct, errors_map) do
fields = struct.__schema__(:fields) |> MapSet.new()
for {key, errors} <- errors_map,
field = String.to_atom(key),
field in fields,
do: {field, errors}
end
defp post(path, json, team \\ nil) do
build_req(team)
|> Req.post(url: path, json: json)
|> handle_response()
|> dispatch_messages(team)
end
defp put(path, json, team) do
build_req(team)
|> Req.put(url: path, json: json)
|> handle_response()
|> dispatch_messages(team)
end
defp delete(path, json, team) do
build_req(team)
|> Req.delete(url: path, json: json)
|> handle_response()
|> dispatch_messages(team)
end
defp get(path, params \\ %{}, team \\ nil) do
build_req(team)
|> Req.get(url: path, params: params)
|> handle_response()
end
defp upload(path, content, params, team) do
build_req(team)
|> Req.Request.put_header("content-length", "#{byte_size(content)}")
|> Req.Request.append_response_steps(
livebook_put_private: fn {request, response} ->
{request, Req.Response.put_private(response, :livebook_app_deployment, true)}
end
)
|> Req.post(url: path, params: params, body: content)
|> handle_response()
|> dispatch_messages(team)
end
defp build_req(team) do
Req.new(base_url: Livebook.Config.teams_url())
|> Req.Request.put_new_header("x-lb-version", Livebook.Config.app_version())
|> Livebook.Utils.req_attach_defaults()
|> add_team_auth(team)
end
defp add_team_auth(req, nil), do: req
defp add_team_auth(req, %{offline: %{}}) do
Req.Request.append_request_steps(req, unauthorized: &{&1, Req.Response.new(status: 401)})
end
defp add_team_auth(req, %{session_token: @org_token_prefix <> _} = team) do
token = "#{team.session_token}:#{Teams.Org.key_hash(%Teams.Org{teams_key: team.teams_key})}"
Req.Request.merge_options(req, auth: {:bearer, token})
end
defp add_team_auth(req, %{user_id: nil} = team) do
agent_name = Livebook.Config.agent_name()
token = "#{team.session_token}:#{agent_name}:#{team.org_id}:#{team.org_key_id}"
Req.Request.merge_options(req, auth: {:bearer, token})
end
defp add_team_auth(req, team) do
token = "#{team.user_id}:#{team.org_id}:#{team.org_key_id}:#{team.session_token}"
Req.Request.merge_options(req, auth: {:bearer, token})
end
defp handle_response(response) do
case response do
{:ok, %{status: status} = response} when status in 200..299 ->
{:ok, response.body}
{:ok, %{status: status} = response} when status in [403, 410, 422] ->
return_error(response)
{:ok, %{status: 401, private: %{livebook_app_deployment: true}}} ->
{:transport_error, @unauthorized_app_deployment_error_message}
{:ok, %{status: 401}} ->
{:transport_error, @unauthorized_error_message}
_otherwise ->
{:transport_error, @error_message}
end
end
defp dispatch_messages({:ok, %{"messages" => _} = body}, %Livebook.Hubs.Team{} = team) do
{messages, body} = Map.pop!(body, "messages")
for message <- messages do
%{type: event} =
message
|> Base.url_decode64!(padding: false)
|> LivebookProto.Event.decode()
Livebook.Hubs.TeamClient.handle_event(team.id, event)
end
{:ok, body}
end
defp dispatch_messages(result, _), do: result
defp return_error(response) do
if json?(response),
do: {:error, response.body},
else: {:transport_error, response.body}
end
defp json?(response) do
"application/json; charset=utf-8" in Req.Response.get_header(response, "content-type")
end
end