Packages
livebook
0.14.0-rc.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/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
alias Livebook.Teams.{AppDeployment, DeploymentGroup, Org}
@error_message "Something went wrong, try again later or please file a bug if it persists"
@doc """
Send a request to Livebook Team API to create a new org.
"""
@spec create_org(Org.t()) ::
{:ok, map()} | {:error, map() | String.t()} | {:transport_error, String.t()}
def create_org(org) do
post("/api/v1/org-request", %{name: org.name, key_hash: Org.key_hash(org)})
end
@doc """
Send a request to Livebook Team API to join an org.
"""
@spec join_org(Org.t()) ::
{:ok, map()} | {:error, map() | String.t()} | {:transport_error, String.t()}
def join_org(org) do
post("/api/v1/org-request/join", %{name: org.name, key_hash: 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) ::
{:ok, map()} | {:error, map() | String.t()} | {:transport_error, String.t()}
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()) ::
{:ok, map()} | {:error, map() | String.t()} | {:transport_error, String.t()}
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()) ::
{:ok, map()} | {:error, map() | String.t()} | {:transport_error, String.t()}
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()) ::
{:ok, map()} | {:error, map() | String.t()} | {:transport_error, String.t()}
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()) ::
{:ok, map()} | {:error, map() | String.t()} | {:transport_error, String.t()}
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()) ::
{:ok, map()} | {:error, map() | String.t()} | {:transport_error, String.t()}
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 = Jason.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()) ::
{:ok, map()} | {:error, map() | String.t()} | {:transport_error, String.t()}
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 = Jason.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()) ::
{:ok, map()} | {:error, map() | String.t()} | {:transport_error, String.t()}
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(), DeploymentGroup.t()) ::
{:ok, map()} | {:error, map() | String.t()} | {:transport_error, String.t()}
def create_deployment_group(team, deployment_group) do
params = %{
name: deployment_group.name,
mode: deployment_group.mode,
clustering: deployment_group.clustering,
zta_provider: deployment_group.zta_provider,
zta_key: deployment_group.zta_key,
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(), AppDeployment.t()) ::
{:ok, map()} | {:error, map() | String.t()} | {:transport_error, String.t()}
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(), AppDeployment.t()) ::
{:ok, binary()} | {:error, map() | String.t()} | {:transport_error, String.t()}
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 """
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
@doc false
def error_message(), do: @error_message
defp post(path, json, team \\ nil) do
build_req()
|> add_team_auth(team)
|> request(method: :post, url: path, json: json)
|> dispatch_messages(team)
end
defp put(path, json, team) do
build_req()
|> add_team_auth(team)
|> request(method: :put, url: path, json: json)
|> dispatch_messages(team)
end
defp delete(path, json, team) do
build_req()
|> add_team_auth(team)
|> request(method: :delete, url: path, json: json)
|> dispatch_messages(team)
end
defp get(path, params \\ %{}, team \\ nil) do
build_req()
|> add_team_auth(team)
|> request(method: :get, url: path, params: params)
end
defp upload(path, content, params, team) do
build_req()
|> add_team_auth(team)
|> Req.Request.put_header("content-length", "#{byte_size(content)}")
|> request(method: :post, url: path, params: params, body: content)
|> dispatch_messages(team)
end
defp build_req() do
Req.new(
base_url: Livebook.Config.teams_url(),
inet6: String.ends_with?(Livebook.Config.teams_url(), ".flycast"),
headers: [{"x-lb-version", Livebook.Config.app_version()}]
)
end
defp add_team_auth(req, nil), do: req
defp add_team_auth(req, team) do
if team.offline do
Req.Request.append_request_steps(req,
unauthorized: fn req ->
{req, Req.Response.new(status: 401)}
end
)
else
token =
if team.user_id do
"#{team.user_id}:#{team.org_id}:#{team.org_key_id}:#{team.session_token}"
else
"#{team.session_token}:#{Livebook.Config.agent_name()}:#{team.org_id}:#{team.org_key_id}"
end
Req.Request.merge_options(req, auth: {:bearer, token})
end
end
defp request(req, opts) do
case Req.request(req, opts) do
{:ok, %{status: 204, body: body}} ->
{:ok, body}
{:ok, %{status: status} = response} when status in 200..299 ->
{:ok, response.body}
{:ok, %{status: status} = response} when status in [410, 422] ->
if json?(response),
do: {:error, response.body},
else: {:transport_error, response.body}
{:ok, %{status: 401}} ->
{:transport_error,
"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"}
_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 json?(response) do
"application/json; charset=utf-8" in Req.Response.get_header(response, "content-type")
end
end