mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-09-04 20:14:57 +08:00
Improvements
This commit is contained in:
parent
803ce3cf9c
commit
bd5ff093c0
2 changed files with 250 additions and 71 deletions
|
@ -1,6 +1,4 @@
|
|||
defmodule LivebookCLI do
|
||||
import Kernel, except: [raise: 1]
|
||||
|
||||
@switches [
|
||||
help: :boolean,
|
||||
version: :boolean
|
||||
|
@ -103,32 +101,97 @@ defmodule LivebookCLI do
|
|||
@doc """
|
||||
Logs an error message.
|
||||
"""
|
||||
def error(message), do: IO.puts(:stderr, IO.ANSI.format(red(message)))
|
||||
def error(message) do
|
||||
message
|
||||
|> put_level(:error)
|
||||
|> IO.ANSI.format()
|
||||
|> IO.puts()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Logs an info message.
|
||||
"""
|
||||
def info(message), do: IO.puts(IO.ANSI.format(message))
|
||||
def info(message) do
|
||||
message
|
||||
|> put_level(:info)
|
||||
|> IO.ANSI.format()
|
||||
|> IO.puts()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Logs a debug message.
|
||||
"""
|
||||
def debug(message), do: IO.puts(IO.ANSI.format(cyan(message)))
|
||||
if Mix.env() == :dev do
|
||||
def debug(message) do
|
||||
message
|
||||
|> put_level(:debug)
|
||||
|> IO.ANSI.format()
|
||||
|> IO.puts()
|
||||
end
|
||||
else
|
||||
def debug(message) do
|
||||
_ = put_level(message, :debug)
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Logs a warning message.
|
||||
"""
|
||||
def warning(message), do: IO.warn(message, [])
|
||||
def warning(message) do
|
||||
message
|
||||
|> put_level(:warning)
|
||||
|> IO.ANSI.format()
|
||||
|> IO.warn()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Aborts
|
||||
Logs an error message.
|
||||
"""
|
||||
@spec raise(String.t()) :: no_return()
|
||||
def raise(message) do
|
||||
error(message)
|
||||
def raise(exception, command_name, stacktrace) when is_exception(exception) do
|
||||
exception
|
||||
|> format_exception(command_name, stacktrace)
|
||||
|> IO.ANSI.format()
|
||||
|> IO.puts()
|
||||
|
||||
System.halt(1)
|
||||
end
|
||||
|
||||
defp red(message), do: [:red, :bright, message]
|
||||
defp cyan(message), do: [:cyan, :bright, message]
|
||||
defp put_level(message, :info) do
|
||||
["[info] ", message]
|
||||
end
|
||||
|
||||
defp put_level(message, :error) do
|
||||
[:red, "[error] ", message]
|
||||
end
|
||||
|
||||
defp put_level(message, :warning) do
|
||||
["[warning] ", message]
|
||||
end
|
||||
|
||||
defp put_level(message, :debug) do
|
||||
[:cyan, "[debug] ", message]
|
||||
end
|
||||
|
||||
defp format_exception(%OptionParser.ParseError{} = exception, command_name, _) do
|
||||
[
|
||||
:red,
|
||||
:bright,
|
||||
"""
|
||||
#{Exception.message(exception)}
|
||||
|
||||
For more information try:
|
||||
|
||||
livebook #{command_name} --help
|
||||
"""
|
||||
]
|
||||
end
|
||||
|
||||
defp format_exception(exception, _command_name, stacktrace) do
|
||||
[
|
||||
:red,
|
||||
:bright,
|
||||
Exception.format(:error, exception, stacktrace)
|
||||
]
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,51 +1,34 @@
|
|||
defmodule LivebookCLI.Deploy do
|
||||
@moduledoc """
|
||||
Usage: livebook deploy [options] filename|directory
|
||||
|
||||
## Available options
|
||||
|
||||
--deploy-key Sets the deploy key to authenticate with Livebook Teams
|
||||
--teams-key Sets the Teams key to encrypt the Livebook app
|
||||
--dg The deployment group name which you want to deploy
|
||||
|
||||
The --help option can be given to print this notice.
|
||||
|
||||
## Examples
|
||||
|
||||
Deploys a single notebook:
|
||||
|
||||
livebook deploy --deploy-key="lb_dk_..." --teams-key="lb_tk_..." -dg "online" path/to/file.livemd
|
||||
|
||||
Deploys a folder:
|
||||
|
||||
livebook deploy --deploy-key="lb_dk_..." --teams-key="lb_tk_..." -dg "online" path/to\
|
||||
"""
|
||||
use LivebookCLI.Command
|
||||
|
||||
@impl true
|
||||
def usage() do
|
||||
"""
|
||||
Usage: livebook deploy [options] filename|directory
|
||||
|
||||
## Available options
|
||||
|
||||
--api-key Sets the API key to authenticate with Livebook Teams
|
||||
--teams-key Sets the Teams key to encrypt the Livebook app
|
||||
--dg The deployment group name which you want to deploy
|
||||
|
||||
The --help option can be given to print this notice.
|
||||
|
||||
## Environment variables
|
||||
|
||||
The following environment variables can be used to configure Livebook on boot:
|
||||
|
||||
* `LIVEBOOK_TEAMS_API_KEY` - sets the API key to authenticate with Livebook Teams
|
||||
|
||||
* `LIVEBOOK_TEAMS_KEY` - sets the Teams key to encrypt the Livebook app
|
||||
|
||||
## Examples
|
||||
|
||||
Deploys a single notebook:
|
||||
|
||||
livebook deploy --api-key="lb_ak_..." --teams-key="lb_tk_..." -dg "online" path/to/file.livemd
|
||||
|
||||
Deploys a folder:
|
||||
|
||||
livebook deploy --api-key="lb_ak_..." --teams-key="lb_tk_..." -dg "online" path/to
|
||||
|
||||
Deploys from environment variable:
|
||||
|
||||
export LIVEBOOK_TEAMS_KEY=lb_tk...
|
||||
export LIVEBOOK_TEAMS_API_KEY=lb_ak_...
|
||||
livebook deploy -dg "online" path/to/file.livemd
|
||||
|
||||
Deploys an imported notebook at the given URL:
|
||||
|
||||
livebook deploy --api-key="lb_ak_..." --teams-key="lb_tk_..." -dg "online" https://example.com/my-notebook.livemd\
|
||||
"""
|
||||
@moduledoc
|
||||
end
|
||||
|
||||
@switches [
|
||||
api_key: :string,
|
||||
deploy_key: :string,
|
||||
teams_key: :string,
|
||||
dg: :string
|
||||
]
|
||||
|
@ -53,28 +36,40 @@ defmodule LivebookCLI.Deploy do
|
|||
@impl true
|
||||
def call(args) do
|
||||
Application.put_env(:livebook, :mode, :cli)
|
||||
|
||||
{:ok, _} = Application.ensure_all_started(:livebook)
|
||||
config = config_from_args(args)
|
||||
|
||||
with :ok <- validate_config(config),
|
||||
{:ok, session_auth} <- authenticate_cli(config) do
|
||||
deploy(session_auth, config.path)
|
||||
{:ok, attrs} <- authenticate_cli(config) do
|
||||
team = fetch_or_create_hub(attrs, config)
|
||||
deploy_to_teams(team, config, attrs)
|
||||
end
|
||||
end
|
||||
|
||||
defp config_from_args(args) do
|
||||
{opts, filename_or_directory} = OptionParser.parse!(args, strict: @switches)
|
||||
Enum.into(opts, %{path: filename_or_directory})
|
||||
filename_or_directory = Path.expand(filename_or_directory)
|
||||
|
||||
%{
|
||||
teams_key: opts[:teams_key],
|
||||
deploy_key: opts[:deploy_key],
|
||||
deployment_group_name: opts[:dg],
|
||||
path: filename_or_directory
|
||||
}
|
||||
end
|
||||
|
||||
defp validate_config(config) do
|
||||
cond do
|
||||
config[:api_key] == nil or not String.starts_with?(config.api_key, "lb_ak_") ->
|
||||
{:error, "must be a Livebook Teams API Key"}
|
||||
debug("Validating config from options...")
|
||||
|
||||
config[:teams_key] == nil or not String.starts_with?(config.teams_key, "lb_tk_") ->
|
||||
cond do
|
||||
config.deploy_key == nil or not String.starts_with?(config.deploy_key, "lb_dk_") ->
|
||||
{:error, "must be a Livebook Teams Deploy Key"}
|
||||
|
||||
config.teams_key == nil or not String.starts_with?(config.teams_key, "lb_tk_") ->
|
||||
{:error, "must be a Livebook Teams Key"}
|
||||
|
||||
config[:dg] in ["", nil] ->
|
||||
config.deployment_group_name in ["", nil] ->
|
||||
{:error, "must be a deployment group name"}
|
||||
|
||||
not File.exists?(config.path) ->
|
||||
|
@ -86,28 +81,149 @@ defmodule LivebookCLI.Deploy do
|
|||
end
|
||||
|
||||
defp authenticate_cli(config) do
|
||||
req = Req.new(base_url: Livebook.Config.teams_url(), auth: {:bearer, config.api_key})
|
||||
json = %{key_hash: Livebook.Teams.Org.key_hash(config), deployment_group_name: config.dg}
|
||||
info("Authenticating CLI...")
|
||||
|
||||
case Req.post(req, path: "/api/v1/auth-cli", json: json) do
|
||||
{:ok, %{status: 200, body: session_auth}} -> {:ok, session_auth}
|
||||
case Req.post(client(config), url: "/api/v1/auth-cli") do
|
||||
{:ok, %{status: 200, body: body}} -> {:ok, body}
|
||||
{:ok, %{status: 401}} -> {:error, "Invalid API credentials"}
|
||||
{:ok, %{status: 403}} -> {:error, "You don't have access to this action"}
|
||||
{:ok, %{status: 500}} -> {:error, "Something went wrong"}
|
||||
{:ok, %{status: _, body: body}} -> {:error, error_from_body(body)}
|
||||
{:error, exception} -> Kernel.raise(exception)
|
||||
{:error, exception} -> raise exception
|
||||
end
|
||||
end
|
||||
|
||||
defp error_from_body(%{"error" => %{"details" => error}}) do
|
||||
defp client(config) do
|
||||
debug("Building Req client for: #{inspect(config, pretty: true)}")
|
||||
|
||||
key_hash = Livebook.Teams.Org.key_hash(config.teams_key)
|
||||
token = "#{config.deploy_key}:#{key_hash}:#{config.deployment_group_name}"
|
||||
|
||||
Req.new(base_url: Livebook.Config.teams_url(), auth: {:bearer, token})
|
||||
|> Req.Request.put_new_header("x-lb-version", Livebook.Config.app_version())
|
||||
|> Livebook.Utils.req_attach_defaults()
|
||||
end
|
||||
|
||||
defp error_from_body(%{"errors" => %{"detail" => error}}) do
|
||||
"Livebook Teams API returned error: #{error}"
|
||||
end
|
||||
|
||||
defp deploy(session_auth, path) do
|
||||
for filename <- File.ls(path),
|
||||
path = Path.join(path, filename),
|
||||
not File.dir?(path),
|
||||
defp error_from_body(error) when is_binary(error) do
|
||||
"Livebook Teams API returned error: #{error}"
|
||||
end
|
||||
|
||||
defp fetch_or_create_hub(%{"name" => name} = attrs, config) do
|
||||
id = "team-#{name}"
|
||||
|
||||
if not Livebook.Hubs.hub_exists?(id) do
|
||||
Livebook.Hubs.save_hub(%Livebook.Hubs.Team{
|
||||
id: id,
|
||||
hub_name: name,
|
||||
hub_emoji: "🚀",
|
||||
user_id: nil,
|
||||
org_id: attrs["org_id"],
|
||||
org_key_id: attrs["org_key_id"],
|
||||
session_token: "#{config.deploy_key}:#{config.deployment_group_name}",
|
||||
teams_key: config.teams_key,
|
||||
org_public_key: attrs["public_key"]
|
||||
})
|
||||
|
||||
Application.put_env(:livebook, :teams_auth, :online)
|
||||
Application.put_env(:livebook, :apps_path_hub_id, id)
|
||||
end
|
||||
|
||||
Livebook.Hubs.fetch_hub!(id)
|
||||
end
|
||||
|
||||
defp deploy_to_teams(team, config, %{"url" => url}) do
|
||||
for path <- list_notebooks(config.path),
|
||||
String.ends_with?(path, ".livemd") do
|
||||
{notebook, %{warnings: []}} = Livebook.LiveMarkdown.notebook_from_livemd(source)
|
||||
deploy_notebook(team, path, url)
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp list_notebooks(path) do
|
||||
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)
|
||||
else
|
||||
[path]
|
||||
end
|
||||
|
||||
if files == [] do
|
||||
raise "There's no notebook available to deploy"
|
||||
else
|
||||
if length(files) == 1 do
|
||||
debug("Found 1 notebook")
|
||||
else
|
||||
debug("Found #{length(files)} notebooks")
|
||||
end
|
||||
|
||||
files
|
||||
end
|
||||
end
|
||||
|
||||
defp deploy_notebook(team, path, url) do
|
||||
info("Deploying notebook: #{path}")
|
||||
files_dir = Livebook.FileSystem.File.local(path)
|
||||
|
||||
with {:ok, content} <- File.read(path),
|
||||
{:ok, notebook} = notebook_from_livemd(content),
|
||||
{:ok, app_deployment} <- prepare_app_deployment(notebook, files_dir) do
|
||||
case Livebook.Teams.deploy_app(team, app_deployment) do
|
||||
:ok -> print_deployment(app_deployment, url)
|
||||
{:error, _changeset} -> raise "Invalid data"
|
||||
{:transport_error, reason} -> raise reason
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp notebook_from_livemd(content) do
|
||||
debug("Loading notebook from content")
|
||||
|
||||
case Livebook.LiveMarkdown.notebook_from_livemd(content) do
|
||||
{notebook, %{warnings: [], stamp_verified?: true}} ->
|
||||
debug("Notebook loaded for app: #{notebook.app_settings.slug}")
|
||||
{:ok, notebook}
|
||||
|
||||
{_notebook, %{warnings: [], stamp_verified?: false}} ->
|
||||
{:error, "Failed to validate the notebook stamp"}
|
||||
|
||||
{_notebook, %{warnings: warnings}} ->
|
||||
{:error, Enum.join(warnings, "\n")}
|
||||
end
|
||||
end
|
||||
|
||||
defp prepare_app_deployment(notebook, files_dir) do
|
||||
debug("Preparing app deployment for #{notebook.app_settings.slug}")
|
||||
|
||||
case Livebook.Teams.AppDeployment.new(notebook, files_dir) do
|
||||
{:ok, app_deployment} -> {:ok, app_deployment}
|
||||
{:warning, warnings} -> {:error, Enum.join(warnings, "\n")}
|
||||
other -> other
|
||||
end
|
||||
end
|
||||
|
||||
defp print_deployment(app_deployment, url) do
|
||||
slug =
|
||||
if url do
|
||||
"#{app_deployment.slug} (#{url}/apps/#{app_deployment.slug})"
|
||||
else
|
||||
app_deployment.slug
|
||||
end
|
||||
|
||||
info("""
|
||||
App deployment created successfully.
|
||||
|
||||
Slug: #{slug}
|
||||
Title: #{app_deployment.title}
|
||||
""")
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Add table
Reference in a new issue