Apply review comments

This commit is contained in:
Alexandre de Souza 2025-07-24 12:14:50 -03:00
parent 066df147ed
commit 081d84867d
No known key found for this signature in database
GPG key ID: E39228FFBA346545
14 changed files with 161 additions and 176 deletions

View file

@ -94,23 +94,17 @@ defmodule Livebook.Hubs.Team do
changeset changeset
end end
end end
@doc """
Returns the public key prefix
"""
@spec public_key_prefix() :: String.t()
def public_key_prefix(), do: "lb_opk_"
end end
defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Team do defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Team do
alias Livebook.Hubs.{Team, TeamClient} alias Livebook.Hubs.{Team, TeamClient}
alias Livebook.Teams.Requests alias Livebook.Teams
alias Livebook.FileSystem alias Livebook.FileSystem
alias Livebook.Secrets.Secret alias Livebook.Secrets.Secret
@teams_key_prefix Livebook.Teams.Org.teams_key_prefix() @teams_key_prefix Teams.Constants.teams_key_prefix()
@public_key_prefix Team.public_key_prefix() @public_key_prefix Teams.Constants.public_key_prefix()
@deploy_key_prefix Requests.deploy_key_prefix() @deploy_key_prefix Teams.Constants.deploy_key_prefix()
def load(team, fields) do def load(team, fields) do
{offline?, fields} = Map.pop(fields, :offline?, false) {offline?, fields} = Map.pop(fields, :offline?, false)
@ -162,7 +156,7 @@ defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Team do
@teams_key_prefix <> teams_key = team.teams_key @teams_key_prefix <> teams_key = team.teams_key
token = Livebook.Stamping.chapoly_encrypt(metadata, notebook_source, teams_key) token = Livebook.Stamping.chapoly_encrypt(metadata, notebook_source, teams_key)
case Requests.org_sign(team, token) do case Teams.Requests.org_sign(team, token) do
{:ok, %{"signature" => token_signature}} -> {:ok, %{"signature" => token_signature}} ->
stamp = %{"version" => 1, "token" => token, "token_signature" => token_signature} stamp = %{"version" => 1, "token" => token, "token_signature" => token_signature}
{:ok, stamp} {:ok, stamp}
@ -203,7 +197,7 @@ defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Team do
def get_secrets(team), do: TeamClient.get_secrets(team.id) def get_secrets(team), do: TeamClient.get_secrets(team.id)
def create_secret(%Team{} = team, %Secret{} = secret) do def create_secret(%Team{} = team, %Secret{} = secret) do
case Requests.create_secret(team, secret) do case Teams.Requests.create_secret(team, secret) do
{:ok, %{"id" => _}} -> :ok {:ok, %{"id" => _}} -> :ok
{:error, %{"errors" => errors}} -> {:error, parse_secret_errors(errors)} {:error, %{"errors" => errors}} -> {:error, parse_secret_errors(errors)}
any -> any any -> any
@ -211,7 +205,7 @@ defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Team do
end end
def update_secret(%Team{} = team, %Secret{} = secret) do def update_secret(%Team{} = team, %Secret{} = secret) do
case Requests.update_secret(team, secret) do case Teams.Requests.update_secret(team, secret) do
{:ok, %{"id" => _}} -> :ok {:ok, %{"id" => _}} -> :ok
{:error, %{"errors" => errors}} -> {:error, parse_secret_errors(errors)} {:error, %{"errors" => errors}} -> {:error, parse_secret_errors(errors)}
any -> any any -> any
@ -219,7 +213,7 @@ defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Team do
end end
def delete_secret(%Team{} = team, %Secret{} = secret) do def delete_secret(%Team{} = team, %Secret{} = secret) do
case Requests.delete_secret(team, secret) do case Teams.Requests.delete_secret(team, secret) do
{:ok, _} -> :ok {:ok, _} -> :ok
{:error, %{"errors" => errors}} -> {:error, parse_secret_errors(errors)} {:error, %{"errors" => errors}} -> {:error, parse_secret_errors(errors)}
any -> any any -> any
@ -229,7 +223,7 @@ defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Team do
def get_file_systems(team), do: TeamClient.get_file_systems(team.id) def get_file_systems(team), do: TeamClient.get_file_systems(team.id)
def create_file_system(%Team{} = team, file_system) do def create_file_system(%Team{} = team, file_system) do
case Requests.create_file_system(team, file_system) do case Teams.Requests.create_file_system(team, file_system) do
{:ok, %{"id" => _}} -> :ok {:ok, %{"id" => _}} -> :ok
{:error, %{"errors" => errors}} -> {:error, parse_file_system_errors(file_system, errors)} {:error, %{"errors" => errors}} -> {:error, parse_file_system_errors(file_system, errors)}
any -> any any -> any
@ -237,7 +231,7 @@ defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Team do
end end
def update_file_system(%Team{} = team, file_system) do def update_file_system(%Team{} = team, file_system) do
case Requests.update_file_system(team, file_system) do case Teams.Requests.update_file_system(team, file_system) do
{:ok, %{"id" => _}} -> :ok {:ok, %{"id" => _}} -> :ok
{:error, %{"errors" => errors}} -> {:error, parse_file_system_errors(file_system, errors)} {:error, %{"errors" => errors}} -> {:error, parse_file_system_errors(file_system, errors)}
any -> any any -> any
@ -245,7 +239,7 @@ defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Team do
end end
def delete_file_system(%Team{} = team, file_system) do def delete_file_system(%Team{} = team, file_system) do
case Requests.delete_file_system(team, file_system) do case Teams.Requests.delete_file_system(team, file_system) do
{:ok, _} -> :ok {:ok, _} -> :ok
{:error, %{"errors" => errors}} -> {:error, parse_file_system_errors(file_system, errors)} {:error, %{"errors" => errors}} -> {:error, parse_file_system_errors(file_system, errors)}
any -> any any -> any
@ -266,12 +260,12 @@ defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Team do
end end
defp parse_secret_errors(errors_map) do defp parse_secret_errors(errors_map) do
Requests.to_error_list(Secret, errors_map) Teams.Requests.to_error_list(Secret, errors_map)
end end
defp parse_file_system_errors(%struct{} = file_system, errors_map) do defp parse_file_system_errors(%struct{} = file_system, errors_map) do
%{error_field: field} = FileSystem.external_metadata(file_system) %{error_field: field} = FileSystem.external_metadata(file_system)
errors_map = Map.new(errors_map, fn {_key, values} -> {field, values} end) errors_map = Map.new(errors_map, fn {_key, values} -> {field, values} end)
Requests.to_error_list(struct, errors_map) Teams.Requests.to_error_list(struct, errors_map)
end end
end end

View file

@ -187,15 +187,9 @@ defmodule Livebook.Storage do
Process.flag(:trap_exit, true) Process.flag(:trap_exit, true)
persist_storage? = Application.get_env(:livebook, :persist_storage, true) persist_storage? = Application.get_env(:livebook, :persist_storage, true)
table = load_or_create_table(persist_storage?)
table =
if persist_storage? do
load_or_create_table()
else
:ets.new(__MODULE__, [:protected, :duplicate_bag])
end
:persistent_term.put(__MODULE__, table) :persistent_term.put(__MODULE__, table)
{:ok, %{table: table, persist?: persist_storage?}} {:ok, %{table: table, persist?: persist_storage?}}
end end
@ -239,7 +233,11 @@ defmodule Livebook.Storage do
defp table_name(), do: :persistent_term.get(__MODULE__) defp table_name(), do: :persistent_term.get(__MODULE__)
defp load_or_create_table() do defp load_or_create_table(false) do
:ets.new(__MODULE__, [:protected, :duplicate_bag])
end
defp load_or_create_table(true) do
tab = tab =
if path = config_file_path_for_restore() do if path = config_file_path_for_restore() do
path path

View file

@ -11,7 +11,7 @@ defmodule Livebook.Teams do
import Ecto.Changeset, import Ecto.Changeset,
only: [add_error: 3, apply_action: 2, apply_action!: 2, get_field: 2] only: [add_error: 3, apply_action: 2, apply_action!: 2, get_field: 2]
@prefix Org.teams_key_prefix() @teams_key_prefix Teams.Constants.teams_key_prefix()
@doc """ @doc """
Creates an Org. Creates an Org.
@ -148,7 +148,7 @@ defmodule Livebook.Teams do
Derives the secret and sign secret from given `teams_key`. Derives the secret and sign secret from given `teams_key`.
""" """
@spec derive_key(String.t()) :: bitstring() @spec derive_key(String.t()) :: bitstring()
def derive_key(@prefix <> teams_key) do def derive_key(@teams_key_prefix <> teams_key) do
binary_key = Base.url_decode64!(teams_key, padding: false) binary_key = Base.url_decode64!(teams_key, padding: false)
Plug.Crypto.KeyGenerator.generate(binary_key, "notebook secret", cache: Plug.Crypto.Keys) Plug.Crypto.KeyGenerator.generate(binary_key, "notebook secret", cache: Plug.Crypto.Keys)
end end
@ -245,21 +245,17 @@ defmodule Livebook.Teams do
id = "team-#{name}" id = "team-#{name}"
hub = hub =
if Hubs.hub_exists?(id) do Hubs.save_hub(%Team{
%{Hubs.fetch_hub!(id) | user_id: nil, session_token: config.session_token} id: id,
else hub_name: name,
Hubs.save_hub(%Team{ hub_emoji: "🚀",
id: id, user_id: nil,
hub_name: name, org_id: attrs["org_id"],
hub_emoji: "🚀", org_key_id: attrs["org_key_id"],
user_id: nil, session_token: config.session_token,
org_id: attrs["org_id"], teams_key: config.teams_key,
org_key_id: attrs["org_key_id"], org_public_key: attrs["public_key"]
session_token: config.session_token, })
teams_key: config.teams_key,
org_public_key: attrs["public_key"]
})
end
{:ok, hub} {:ok, hub}
end end

View file

@ -0,0 +1,25 @@
defmodule Livebook.Teams.Constants do
@doc """
Returns the public key prefix
"""
@spec public_key_prefix() :: String.t()
def public_key_prefix(), do: "lb_opk_"
@doc """
Returns the Agent Key prefix
"""
@spec agent_key_prefix() :: String.t()
def agent_key_prefix, do: "lb_ak_"
@doc """
Returns the Deploy Key prefix
"""
@spec deploy_key_prefix() :: String.t()
def deploy_key_prefix, do: "lb_dk_"
@doc """
Returns the Teams Key prefix
"""
@spec teams_key_prefix() :: String.t()
def teams_key_prefix(), do: "lb_tk_"
end

View file

@ -2,8 +2,6 @@ defmodule Livebook.Teams.Org do
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset
@prefix "lb_tk_"
@type t :: %__MODULE__{ @type t :: %__MODULE__{
id: pos_integer() | nil, id: pos_integer() | nil,
emoji: String.t() | nil, emoji: String.t() | nil,
@ -31,7 +29,7 @@ defmodule Livebook.Teams.Org do
@spec teams_key() :: String.t() @spec teams_key() :: String.t()
def teams_key() do def teams_key() do
key = :crypto.strong_rand_bytes(@secret_key_size) key = :crypto.strong_rand_bytes(@secret_key_size)
@prefix <> Base.url_encode64(key, padding: false) Livebook.Teams.Constants.teams_key_prefix() <> Base.url_encode64(key, padding: false)
end end
@doc """ @doc """
@ -50,10 +48,4 @@ defmodule Livebook.Teams.Org do
message: "should only contain lowercase alphanumeric characters and dashes" message: "should only contain lowercase alphanumeric characters and dashes"
) )
end end
@doc """
Returns the teams key prefix
"""
@spec teams_key_prefix() :: String.t()
def teams_key_prefix(), do: @prefix
end end

View file

@ -5,7 +5,7 @@ defmodule Livebook.Teams.Requests do
alias Livebook.Secrets.Secret alias Livebook.Secrets.Secret
alias Livebook.Teams alias Livebook.Teams
@deploy_key_prefix "lb_dk_" @deploy_key_prefix Teams.Constants.deploy_key_prefix()
@error_message "Something went wrong, try again later or please file a bug if it persists" @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_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"
@ -15,9 +15,6 @@ defmodule Livebook.Teams.Requests do
@doc false @doc false
def error_message(), do: @error_message def error_message(), do: @error_message
@doc false
def deploy_key_prefix(), do: @deploy_key_prefix
@doc """ @doc """
Send a request to Livebook Team API to create a new org. Send a request to Livebook Team API to create a new org.
""" """
@ -303,8 +300,6 @@ defmodule Livebook.Teams.Requests do
defp upload(path, content, params, team) do defp upload(path, content, params, team) do
build_req(team) build_req(team)
|> Req.Request.put_header("content-length", "#{byte_size(content)}") |> Req.Request.put_header("content-length", "#{byte_size(content)}")
|> Req.Request.put_private(:cli, path =~ "cli")
|> Req.Request.put_private(:deploy, true)
|> Req.post(url: path, params: params, body: content) |> Req.post(url: path, params: params, body: content)
|> handle_response() |> handle_response()
|> dispatch_messages(team) |> dispatch_messages(team)

View file

@ -21,7 +21,6 @@ defmodule LivebookCLI do
extract_priv!() extract_priv!()
:ok = Application.load(:livebook) :ok = Application.load(:livebook)
Application.put_env(:livebook, :persist_storage, false)
if unix?() do if unix?() do
Application.put_env(:elixir, :ansi_enabled, true) Application.put_env(:elixir, :ansi_enabled, true)
@ -30,7 +29,7 @@ defmodule LivebookCLI do
case args do case args do
[arg] when arg in @help_args -> display_help() [arg] when arg in @help_args -> display_help()
[arg] when arg in @version_args -> display_version() [arg] when arg in @version_args -> display_version()
[name | [arg]] when arg in @help_args -> Task.usage(name) [name, arg] when arg in @help_args -> Task.usage(name)
[name | args] -> Task.call(name, List.delete(args, name)) [name | args] -> Task.call(name, List.delete(args, name))
_args -> Utils.print_text(usage()) _args -> Utils.print_text(usage())
end end

View file

@ -4,8 +4,8 @@ defmodule LivebookCLI.Deploy do
@behaviour LivebookCLI.Task @behaviour LivebookCLI.Task
@deploy_key_prefix Teams.Requests.deploy_key_prefix() @deploy_key_prefix Teams.Constants.deploy_key_prefix()
@teams_key_prefix Teams.Org.teams_key_prefix() @teams_key_prefix Teams.Constants.teams_key_prefix()
@impl true @impl true
def usage() do def usage() do
@ -16,7 +16,7 @@ defmodule LivebookCLI.Deploy do
--deploy-key Sets the deploy key to authenticate with Livebook Teams --deploy-key Sets the deploy key to authenticate with Livebook Teams
--teams-key Sets the Teams key to authenticate with Livebook Teams and encrypt the Livebook app --teams-key Sets the Teams key to authenticate with Livebook Teams and encrypt the Livebook app
--deployment-group The deployment group name which you want to deploy --deployment-group The deployment group name which you want to deploy to
The --help option can be given to print this notice. The --help option can be given to print this notice.
@ -24,11 +24,11 @@ defmodule LivebookCLI.Deploy do
Deploys a single notebook: Deploys a single notebook:
livebook deploy --deploy-key="lb_dk_..." --teams-key="lb_tk_..." -deployment-group "online" path/to/file.livemd livebook deploy --deploy-key="lb_dk_..." --teams-key="lb_tk_..." --deployment-group "online" path/to/app1.livemd
Deploys a folder: Deploys multiple notebooks:
livebook deploy --deploy-key="lb_dk_..." --teams-key="lb_tk_..." -deployment-group "online" path/to\ livebook deploy --deploy-key="lb_dk_..." --teams-key="lb_tk_..." --deployment-group "online" path/to/*.livemd\
""" """
end end
@ -40,6 +40,7 @@ defmodule LivebookCLI.Deploy do
@impl true @impl true
def call(args) do def call(args) do
Application.put_env(:livebook, :persist_storage, false)
{:ok, _} = Application.ensure_all_started(:livebook) {:ok, _} = Application.ensure_all_started(:livebook)
config = config_from_args(args) config = config_from_args(args)
ensure_config!(config) ensure_config!(config)
@ -49,11 +50,10 @@ defmodule LivebookCLI.Deploy do
end end
defp config_from_args(args) do defp config_from_args(args) do
{opts, filename_or_directory} = OptionParser.parse!(args, strict: @switches) {opts, path} = OptionParser.parse!(args, strict: @switches)
filename_or_directory = Path.expand(filename_or_directory)
%{ %{
path: filename_or_directory, path: List.flatten(path),
session_token: opts[:deploy_key], session_token: opts[:deploy_key],
teams_key: opts[:teams_key], teams_key: opts[:teams_key],
deployment_group: opts[:deployment_group] deployment_group: opts[:deployment_group]
@ -83,11 +83,7 @@ defmodule LivebookCLI.Deploy do
end end
{:path, value}, acc -> {:path, value}, acc ->
if not File.exists?(value) do Enum.reduce_while(value, acc, &validate_path/2)
add_error(acc, normalize_key(:path), "must be a valid path")
else
acc
end
_otherwise, acc -> _otherwise, acc ->
acc acc
@ -96,7 +92,7 @@ defmodule LivebookCLI.Deploy do
if Map.keys(errors) == [] do if Map.keys(errors) == [] do
:ok :ok
else else
raise """ raise LivebookCLI.Error, """
You configuration is invalid, make sure you are using the correct options for this task. You configuration is invalid, make sure you are using the correct options for this task.
#{format_errors(errors, " * ")}\ #{format_errors(errors, " * ")}\
@ -104,21 +100,39 @@ defmodule LivebookCLI.Deploy do
end end
end end
defp validate_path(value, acc) do
cond do
not File.exists?(value) ->
{:halt, add_error(acc, normalize_key(:path), "must be a valid path")}
File.dir?(value) ->
{:halt, add_error(acc, normalize_key(:path), "must be a file path")}
true ->
{:cont, acc}
end
end
defp authenticate_cli!(config) do defp authenticate_cli!(config) do
log_debug("Authenticating CLI...") log_debug("Authenticating CLI...")
case Teams.fetch_cli_session(config) do case Teams.fetch_cli_session(config) do
{:ok, team} -> team {:ok, team} -> team
{:error, error} -> raise error {:error, error} -> raise LivebookCLI.Error, error
{:transport_error, error} -> raise error {:transport_error, error} -> raise LivebookCLI.Error, error
end end
end end
defp deploy_to_teams(team, config) do defp deploy_to_teams(team, config) do
notebook_paths = list_notebooks!(config.path) if length(config.path) == 1 do
log_debug("Found 1 notebook")
else
log_debug("Found #{length(config.path)} notebooks")
end
log_info("Deploying notebooks:") log_info("Deploying notebooks:")
for path <- notebook_paths do for path <- config.path do
log_info(" * Preparing to deploy notebook #{Path.basename(path)}") log_info(" * Preparing to deploy notebook #{Path.basename(path)}")
files_dir = Livebook.FileSystem.File.local(path) files_dir = Livebook.FileSystem.File.local(path)
@ -129,18 +143,18 @@ defmodule LivebookCLI.Deploy do
print_text([:green, " * #{app_deployment.title} deployed successfully. (#{url})"]) print_text([:green, " * #{app_deployment.title} deployed successfully. (#{url})"])
{:error, errors} -> {:error, errors} ->
print_text([:red, " * #{app_deployment.title} failed to deployed."]) print_text([:red, " * #{app_deployment.title} failed to deploy."])
errors = normalize_errors(errors) errors = normalize_errors(errors)
raise """ raise LivebookCLI.Error, """
#{format_errors(errors, " * ")} #{format_errors(errors, " * ")}
#{Teams.Requests.error_message()}\ #{Teams.Requests.error_message()}\
""" """
{:transport_error, reason} -> {:transport_error, reason} ->
print_text([:red, " * #{app_deployment.title} failed to deployed."]) print_text([:red, " * #{app_deployment.title} failed to deploy."])
raise reason raise LivebookCLI.Error, reason
end end
end end
end end
@ -148,46 +162,19 @@ defmodule LivebookCLI.Deploy do
:ok :ok
end end
defp list_notebooks!(path) do
log_debug("Listing notebooks from: #{path}")
files =
if File.dir?(path) do
path
|> File.ls!()
|> Enum.map(&Path.join(path, &1))
|> Enum.reject(&File.dir?/1)
|> Enum.filter(&String.ends_with?(&1, ".livemd"))
else
[path]
end
if files == [] do
raise "There's no notebook available to deploy"
else
if length(files) == 1 do
log_debug("Found 1 notebook")
else
log_debug("Found #{length(files)} notebooks")
end
files
end
end
defp prepare_app_deployment(path, content, files_dir) do defp prepare_app_deployment(path, content, files_dir) do
case Livebook.Teams.AppDeployment.new(content, files_dir) do case Livebook.Teams.AppDeployment.new(content, files_dir) do
{:ok, app_deployment} -> {:ok, app_deployment} ->
{:ok, app_deployment} {:ok, app_deployment}
{:warning, warnings} -> {:warning, warnings} ->
raise """ raise LivebookCLI.Error, """
Deployment for notebook #{Path.basename(path)} failed because the notebook has some warnings: Deployment for notebook #{Path.basename(path)} failed because the notebook has some warnings:
#{format_list(warnings, " * ")} #{format_list(warnings, " * ")}
""" """
{:error, reason} -> {:error, reason} ->
raise "Failed to handle I/O operations: #{reason}" raise LivebookCLI.Error, "Failed to handle I/O operations: #{reason}"
end end
end end

View file

@ -0,0 +1,3 @@
defmodule LivebookCLI.Error do
defexception [:message]
end

View file

@ -88,7 +88,8 @@ defmodule LivebookCLI.Server do
open_from_args(base_url, extra_args) open_from_args(base_url, extra_args)
:taken -> :taken ->
raise "Another application is already running on port #{port}." <> raise LivebookCLI.Error,
"Another application is already running on port #{port}." <>
" Either ensure this port is free or specify a different port using the --port option" " Either ensure this port is free or specify a different port using the --port option"
:available -> :available ->
@ -106,7 +107,7 @@ defmodule LivebookCLI.Server do
Process.sleep(:infinity) Process.sleep(:infinity)
{:error, error} -> {:error, error} ->
raise "Livebook failed to start with reason: #{inspect(error)}" raise LivebookCLI.Error, "Livebook failed to start with reason: #{inspect(error)}"
end end
end end
@ -174,7 +175,9 @@ defmodule LivebookCLI.Server do
end end
defp open_from_args(_base_url, _extra_args) do defp open_from_args(_base_url, _extra_args) do
raise "Too many arguments entered. Ensure only one argument is used to specify the file path and all other arguments are preceded by the relevant switch" raise OptionParser.ParseError,
"Too many arguments entered. Ensure only one argument is used to" <>
"specify the file path and all other arguments are preceded by the relevant switch"
end end
@switches [ @switches [

View file

@ -68,8 +68,14 @@ defmodule LivebookCLI.Task do
""" """
end end
defp format_exception(%RuntimeError{} = exception, _, _) do defp format_exception(%LivebookCLI.Error{} = exception, command_name, _) do
Exception.message(exception) """
#{Exception.message(exception)}
For more information try:
livebook #{command_name} --help
"""
end end
defp format_exception(exception, _, stacktrace) do defp format_exception(exception, _, stacktrace) do

View file

@ -4,7 +4,6 @@ defmodule LivebookCLI.Integration.DeployTest do
import Livebook.AppHelpers import Livebook.AppHelpers
alias Livebook.Utils alias Livebook.Utils
alias LivebookCLI.Deploy
@url "http://localhost:4200" @url "http://localhost:4200"
@ -15,7 +14,6 @@ defmodule LivebookCLI.Integration.DeployTest do
@moduletag subscribe_to_teams_topics: [:clients, :deployment_groups, :app_deployments] @moduletag subscribe_to_teams_topics: [:clients, :deployment_groups, :app_deployments]
@moduletag :tmp_dir @moduletag :tmp_dir
@moduletag :capture_io
describe "CLI deploy integration" do describe "CLI deploy integration" do
test "successfully deploys a notebook via CLI", test "successfully deploys a notebook via CLI",
@ -42,15 +40,12 @@ defmodule LivebookCLI.Integration.DeployTest do
output = output =
ExUnit.CaptureIO.capture_io(fn -> ExUnit.CaptureIO.capture_io(fn ->
assert Deploy.call([ assert deploy(
"--deploy-key",
key, key,
"--teams-key",
team.teams_key, team.teams_key,
"--deployment-group",
deployment_group.name, deployment_group.name,
app_path app_path
]) == :ok ) == :ok
end) end)
assert output =~ "* Preparing to deploy notebook #{slug}.livemd" assert output =~ "* Preparing to deploy notebook #{slug}.livemd"
@ -94,15 +89,12 @@ defmodule LivebookCLI.Integration.DeployTest do
output = output =
ExUnit.CaptureIO.capture_io(fn -> ExUnit.CaptureIO.capture_io(fn ->
assert Deploy.call([ assert deploy(
"--deploy-key",
key, key,
"--teams-key",
team.teams_key, team.teams_key,
"--deployment-group",
deployment_group.name, deployment_group.name,
tmp_dir Path.join(tmp_dir, "*.livemd")
]) == :ok ) == :ok
end) end)
for {slug, title} <- apps do for {slug, title} <- apps do
@ -131,16 +123,13 @@ defmodule LivebookCLI.Integration.DeployTest do
# Test App # Test App
""") """)
assert_raise RuntimeError, ~r/Deploy Key must be a Livebook Teams Deploy Key/s, fn -> assert_raise LivebookCLI.Error, ~r/Deploy Key must be a Livebook Teams Deploy Key/s, fn ->
Deploy.call([ deploy(
"--deploy-key",
"invalid_key", "invalid_key",
"--teams-key",
team.teams_key, team.teams_key,
"--deployment-group",
deployment_group.name, deployment_group.name,
app_path app_path
]) )
end end
end end
@ -156,16 +145,13 @@ defmodule LivebookCLI.Integration.DeployTest do
# Test App # Test App
""") """)
assert_raise RuntimeError, ~r/Teams Key must be a Livebook Teams Key/s, fn -> assert_raise LivebookCLI.Error, ~r/Teams Key must be a Livebook Teams Key/s, fn ->
Deploy.call([ deploy(
"--deploy-key",
key, key,
"--teams-key",
"invalid-key", "invalid-key",
"--deployment-group",
deployment_group.name, deployment_group.name,
app_path app_path
]) )
end end
end end
@ -181,14 +167,13 @@ defmodule LivebookCLI.Integration.DeployTest do
# Test App # Test App
""") """)
assert_raise RuntimeError, ~r/Deployment Group can't be blank/s, fn -> assert_raise LivebookCLI.Error, ~r/Deployment Group can't be blank/s, fn ->
Deploy.call([ deploy(
"--deploy-key",
key, key,
"--teams-key",
team.teams_key, team.teams_key,
"",
app_path app_path
]) )
end end
end end
@ -214,17 +199,14 @@ defmodule LivebookCLI.Integration.DeployTest do
``` ```
""") """)
assert_raise RuntimeError, ~r/Deployment Group does not exist/s, fn -> assert_raise LivebookCLI.Error, ~r/Deployment Group does not exist/s, fn ->
ExUnit.CaptureIO.capture_io(fn -> ExUnit.CaptureIO.capture_io(fn ->
Deploy.call([ deploy(
"--deploy-key",
key, key,
"--teams-key",
team.teams_key, team.teams_key,
"--deployment-group",
Utils.random_short_id(), Utils.random_short_id(),
app_path app_path
]) )
end) end)
end end
@ -242,38 +224,42 @@ defmodule LivebookCLI.Integration.DeployTest do
{key, _} = TeamsRPC.create_deploy_key(node, org: org) {key, _} = TeamsRPC.create_deploy_key(node, org: org)
deployment_group = TeamsRPC.create_deployment_group(node, org: org, url: @url) deployment_group = TeamsRPC.create_deployment_group(node, org: org, url: @url)
assert_raise RuntimeError, ~r/Path must be a valid path/s, fn -> assert_raise LivebookCLI.Error, ~r/Path must be a valid path/s, fn ->
Deploy.call([ deploy(
"--deploy-key",
key, key,
"--teams-key",
team.teams_key, team.teams_key,
"--deployment-group",
deployment_group.name, deployment_group.name,
Path.join(tmp_dir, "app.livemd") Path.join(tmp_dir, "app.livemd")
]) )
end end
end end
test "fails when directory contains no notebooks", test "fails with directory argument", %{team: team, node: node, org: org, tmp_dir: tmp_dir} do
%{team: team, node: node, org: org, tmp_dir: tmp_dir} do
{key, _} = TeamsRPC.create_deploy_key(node, org: org) {key, _} = TeamsRPC.create_deploy_key(node, org: org)
deployment_group = TeamsRPC.create_deployment_group(node, org: org, url: @url) deployment_group = TeamsRPC.create_deployment_group(node, org: org, url: @url)
File.write!(Path.join(tmp_dir, "readme.txt"), "No notebooks here") assert_raise LivebookCLI.Error, ~r/Path must be a file path/s, fn ->
File.write!(Path.join(tmp_dir, "config.json"), "{}") deploy(
assert_raise RuntimeError, ~r/There's no notebook available to deploy/, fn ->
Deploy.call([
"--deploy-key",
key, key,
"--teams-key",
team.teams_key, team.teams_key,
"--deployment-group",
deployment_group.name, deployment_group.name,
tmp_dir tmp_dir
]) )
end end
end end
end end
defp deploy(deploy_key, teams_key, deployment_group_name, path) do
path = if Path.wildcard(path) == [], do: path, else: Path.wildcard(path)
LivebookCLI.Deploy.call([
"--deploy-key",
deploy_key,
"--teams-key",
teams_key,
"--deployment-group",
deployment_group_name,
path
])
end
end end

View file

@ -258,6 +258,7 @@ defmodule Livebook.TeamsTest do
test "authenticates the deploy key", %{team: team} do test "authenticates the deploy key", %{team: team} do
config = %{teams_key: team.teams_key, session_token: team.session_token} config = %{teams_key: team.teams_key, session_token: team.session_token}
refute Livebook.Hubs.hub_exists?(team.id)
assert Teams.fetch_cli_session(config) == {:ok, team} assert Teams.fetch_cli_session(config) == {:ok, team}
assert Livebook.Hubs.hub_exists?(team.id) assert Livebook.Hubs.hub_exists?(team.id)
end end
@ -272,7 +273,7 @@ defmodule Livebook.TeamsTest do
{:ok, {:ok,
%Livebook.Hubs.Team{ %Livebook.Hubs.Team{
billing_status: team.billing_status, billing_status: team.billing_status,
hub_emoji: team.hub_emoji, hub_emoji: "🚀",
hub_name: team.hub_name, hub_name: team.hub_name,
id: team.id, id: team.id,
offline: nil, offline: nil,
@ -284,9 +285,8 @@ defmodule Livebook.TeamsTest do
user_id: nil user_id: nil
}} }}
# If the hub already exist, we don't update them from storage
assert Livebook.Hubs.hub_exists?(team.id) assert Livebook.Hubs.hub_exists?(team.id)
refute Livebook.Hubs.fetch_hub!(team.id).session_token == key assert Livebook.Hubs.fetch_hub!(team.id).session_token == key
end end
@tag teams_persisted: false @tag teams_persisted: false

View file

@ -21,7 +21,8 @@ defmodule Livebook.Factory do
org_id: 1, org_id: 1,
user_id: 1, user_id: 1,
org_key_id: 1, org_key_id: 1,
org_public_key: Livebook.Hubs.Team.public_key_prefix() <> Livebook.Utils.random_long_id(), org_public_key:
Livebook.Teams.Constants.public_key_prefix() <> Livebook.Utils.random_long_id(),
teams_key: org.teams_key, teams_key: org.teams_key,
session_token: Livebook.Utils.random_short_id(), session_token: Livebook.Utils.random_short_id(),
offline: nil offline: nil