Skip to content

Commit 01a2977

Browse files
committed
Add task to deploy notebook from CLI
1 parent ccc07af commit 01a2977

5 files changed

Lines changed: 465 additions & 1 deletion

File tree

config/config.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ config :mime, :types, %{
2424

2525
config :livebook,
2626
agent_name: "default",
27+
mode: :app,
2728
allowed_uri_schemes: [],
2829
app_service_name: nil,
2930
app_service_url: nil,

lib/livebook/application.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@ defmodule Livebook.Application do
326326
Application.put_env(:livebook, :apps_path_hub_id, hub_id)
327327
fun
328328

329-
teams_key || auth ->
329+
Application.get_env(:livebook, :mode) == :app and (teams_key || auth) ->
330330
Livebook.Config.abort!(
331331
"You must specify both LIVEBOOK_TEAMS_KEY and LIVEBOOK_TEAMS_AUTH."
332332
)

lib/livebook_cli.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ defmodule LivebookCLI do
3232
Available commands:
3333
3434
livebook server Starts the Livebook web application
35+
livebook deploy Deploys a notebook to Livebook Teams
3536
3637
The --help and --version options can be given instead of a command for usage and versioning information.\
3738
""")

lib/livebook_cli/deploy.ex

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
defmodule LivebookCLI.Deploy do
2+
import LivebookCLI.Utils
3+
alias Livebook.Teams
4+
5+
@behaviour LivebookCLI.Task
6+
7+
@deploy_key_prefix Teams.Requests.deploy_key_prefix()
8+
@teams_key_prefix Teams.Org.teams_key_prefix()
9+
10+
@impl true
11+
def usage() do
12+
"""
13+
Usage: livebook deploy [options] filename|directory
14+
15+
## Available options
16+
17+
--deploy-key Sets the deploy key to authenticate with Livebook Teams
18+
--teams-key Sets the Teams key to authenticate with Livebook Teams and encrypt the Livebook app
19+
--deployment-group The deployment group name which you want to deploy
20+
21+
The --help option can be given to print this notice.
22+
23+
## Examples
24+
25+
Deploys a single notebook:
26+
27+
livebook deploy --deploy-key="lb_dk_..." --teams-key="lb_tk_..." -deployment-group "online" path/to/file.livemd
28+
29+
Deploys a folder:
30+
31+
livebook deploy --deploy-key="lb_dk_..." --teams-key="lb_tk_..." -deployment-group "online" path/to\
32+
"""
33+
end
34+
35+
@switches [
36+
deploy_key: :string,
37+
teams_key: :string,
38+
deployment_group: :string
39+
]
40+
41+
@impl true
42+
def call(args) do
43+
Application.put_env(:livebook, :mode, :cli)
44+
Application.put_env(:livebook, LivebookWeb.Endpoint, server: false)
45+
46+
{:ok, _} = Application.ensure_all_started(:livebook)
47+
config = config_from_args(args)
48+
ensure_config!(config)
49+
50+
team = authenticate_cli!(config)
51+
deploy_to_teams(team, config)
52+
end
53+
54+
defp config_from_args(args) do
55+
{opts, filename_or_directory} = OptionParser.parse!(args, strict: @switches)
56+
filename_or_directory = Path.expand(filename_or_directory)
57+
58+
%{
59+
path: filename_or_directory,
60+
session_token: opts[:deploy_key],
61+
teams_key: opts[:teams_key],
62+
deployment_group: opts[:deployment_group]
63+
}
64+
end
65+
66+
defp ensure_config!(config) do
67+
log_debug("Validating config from options...")
68+
69+
errors =
70+
Enum.reduce(config, %{}, fn
71+
{:session_token, value}, acc when value in ["", nil] ->
72+
add_error(acc, "Deploy Key", "can't be blank")
73+
74+
{:session_token, value}, acc ->
75+
if not String.starts_with?(value, @deploy_key_prefix) do
76+
add_error(acc, "Deploy Key", "must be a Livebook Teams Deploy Key")
77+
else
78+
acc
79+
end
80+
81+
{:teams_key, value}, acc when value in ["", nil] ->
82+
add_error(acc, "Teams Key", "can't be blank")
83+
84+
{:teams_key, value}, acc ->
85+
if not String.starts_with?(value, @teams_key_prefix) do
86+
add_error(acc, "Teams Key", "must be a Livebook Teams Key")
87+
else
88+
acc
89+
end
90+
91+
{:deployment_group, value}, acc when value in ["", nil] ->
92+
add_error(acc, "Deployment Group", "can't be blank")
93+
94+
{:path, value}, acc when value in ["", nil] ->
95+
add_error(acc, "Path", "can't be blank")
96+
97+
{:path, value}, acc ->
98+
if not File.exists?(value) do
99+
add_error(acc, "Path", "must be a valid path")
100+
else
101+
acc
102+
end
103+
104+
_otherwise, acc ->
105+
acc
106+
end)
107+
108+
if Map.keys(errors) == [] do
109+
:ok
110+
else
111+
raise """
112+
You configuration is invalid, make sure you are using the correct options for this task.
113+
114+
#{format_errors(errors)}\
115+
"""
116+
end
117+
end
118+
119+
defp authenticate_cli!(config) do
120+
log_debug("Authenticating CLI...")
121+
122+
case Teams.fetch_cli_session(config) do
123+
{:ok, team} -> team
124+
{:error, error} -> raise error
125+
{:transport_error, error} -> raise error
126+
end
127+
end
128+
129+
defp deploy_to_teams(team, config) do
130+
for path <- list_notebooks!(config.path) do
131+
log_debug("Deploying notebook: #{path}")
132+
files_dir = Livebook.FileSystem.File.local(path)
133+
134+
with {:ok, content} <- File.read(path),
135+
{:ok, app_deployment} <- prepare_app_deployment(path, content, files_dir) do
136+
case Livebook.Teams.deploy_app_from_cli(team, app_deployment, config.deployment_group) do
137+
{:ok, url} -> print_deployment(app_deployment, url)
138+
{:error, errors} -> raise format_errors(errors)
139+
{:transport_error, reason} -> raise reason
140+
end
141+
end
142+
end
143+
144+
:ok
145+
end
146+
147+
defp list_notebooks!(path) do
148+
log_debug("Listing notebooks from: #{path}")
149+
150+
files =
151+
if File.dir?(path) do
152+
path
153+
|> File.ls!()
154+
|> Enum.map(&Path.join(path, &1))
155+
|> Enum.reject(&File.dir?/1)
156+
|> Enum.filter(&String.ends_with?(&1, ".livemd"))
157+
else
158+
[path]
159+
end
160+
161+
if files == [] do
162+
raise "There's no notebook available to deploy"
163+
else
164+
if length(files) == 1 do
165+
log_debug("Found 1 notebook")
166+
else
167+
log_debug("Found #{length(files)} notebooks")
168+
end
169+
170+
files
171+
end
172+
end
173+
174+
defp add_error(errors, key, message) do
175+
Map.update(errors, key, [message], &[message | &1])
176+
end
177+
178+
defp format_errors(%{} = errors_map) do
179+
errors_map
180+
|> Enum.map(fn {key, errors} ->
181+
"""
182+
* #{key}
183+
#{format_list(errors)}\
184+
"""
185+
end)
186+
|> Enum.join("\n")
187+
end
188+
189+
defp format_list(errors) when is_list(errors) do
190+
errors |> Enum.map(&" * #{&1}") |> Enum.join("\n")
191+
end
192+
193+
defp prepare_app_deployment(path, content, files_dir) do
194+
case Livebook.Teams.AppDeployment.new(content, files_dir) do
195+
{:ok, app_deployment} ->
196+
{:ok, app_deployment}
197+
198+
{:warning, warnings} ->
199+
raise """
200+
Deployment for notebook #{Path.basename(path)} failed because the notebook has some warnings:
201+
#{format_list(warnings)}
202+
"""
203+
204+
{:error, reason} ->
205+
raise "Failed to handle I/O operations: #{reason}"
206+
end
207+
end
208+
209+
defp print_deployment(app_deployment, url) do
210+
print_text([
211+
:green,
212+
"App deployment created successfully.\n\n",
213+
:magenta,
214+
:bright,
215+
"Slug: ",
216+
:reset,
217+
:white,
218+
"#{app_deployment.slug} (#{url})\n",
219+
:magenta,
220+
:bright,
221+
"Title: ",
222+
:reset,
223+
:white,
224+
app_deployment.title
225+
])
226+
end
227+
end

0 commit comments

Comments
 (0)