Deployment group for app deployment (#2410)

Co-authored-by: Jonatan Kłosko <jonatanklosko@gmail.com>
Co-authored-by: José Valim <jose.valim@gmail.com>
This commit is contained in:
Cristine Guadelupe 2023-12-27 15:24:48 -03:00 committed by GitHub
parent 8b2add1e6c
commit 37c7444328
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 227 additions and 14 deletions

View file

@ -277,4 +277,6 @@ defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Personal do
:ok = Personal.remove_file_system(file_system.id) :ok = Personal.remove_file_system(file_system.id)
:ok = Broadcasts.file_system_deleted(file_system) :ok = Broadcasts.file_system_deleted(file_system)
end end
def deployment_groups(_personal), do: nil
end end

View file

@ -138,4 +138,13 @@ defprotocol Livebook.Hubs.Provider do
""" """
@spec delete_file_system(t(), FileSystem.t()) :: :ok | {:transport_error, String.t()} @spec delete_file_system(t(), FileSystem.t()) :: :ok | {:transport_error, String.t()}
def delete_file_system(hub, file_system) def delete_file_system(hub, file_system)
@doc """
Get the deployment groups for a given hub.
Returns `nil` if deployment groups are not applicable to this hub.
"""
@spec deployment_groups(t()) ::
list(%{id: String.t(), name: String.t(), secrets: list(Secret.t())}) | nil
def deployment_groups(hub)
end end

View file

@ -280,6 +280,8 @@ defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Team do
end end
end end
def deployment_groups(team), do: TeamClient.get_deployment_groups(team.id)
defp add_secret_errors(%Secret{} = secret, errors_map) do defp add_secret_errors(%Secret{} = secret, errors_map) do
Requests.add_errors(secret, errors_map) Requests.add_errors(secret, errors_map)
end end

View file

@ -102,7 +102,14 @@ defmodule Livebook.LiveMarkdown.Export do
end end
defp notebook_metadata(notebook) do defp notebook_metadata(notebook) do
keys = [:persist_outputs, :autosave_interval_s, :default_language, :hub_id] keys = [
:persist_outputs,
:autosave_interval_s,
:default_language,
:hub_id,
:deployment_group_id
]
metadata = put_unless_default(%{}, Map.take(notebook, keys), Map.take(Notebook.new(), keys)) metadata = put_unless_default(%{}, Map.take(notebook, keys), Map.take(Notebook.new(), keys))
app_settings_metadata = app_settings_metadata(notebook.app_settings) app_settings_metadata = app_settings_metadata(notebook.app_settings)

View file

@ -420,6 +420,9 @@ defmodule Livebook.LiveMarkdown.Import do
{"hub_id", hub_id}, {attrs, messages} -> {"hub_id", hub_id}, {attrs, messages} ->
{Map.put(attrs, :hub_id, hub_id), messages} {Map.put(attrs, :hub_id, hub_id), messages}
{"deployment_group_id", deployment_group_id}, {attrs, messages} ->
{Map.put(attrs, :deployment_group_id, deployment_group_id), messages}
{"app_settings", app_settings_metadata}, {attrs, messages} -> {"app_settings", app_settings_metadata}, {attrs, messages} ->
app_settings = app_settings =
Map.merge( Map.merge(

View file

@ -25,7 +25,8 @@ defmodule Livebook.Notebook do
:hub_secret_names, :hub_secret_names,
:file_entries, :file_entries,
:quarantine_file_entry_names, :quarantine_file_entry_names,
:teams_enabled :teams_enabled,
:deployment_group_id
] ]
alias Livebook.Notebook.{Section, Cell, AppSettings} alias Livebook.Notebook.{Section, Cell, AppSettings}
@ -46,7 +47,8 @@ defmodule Livebook.Notebook do
hub_secret_names: list(String.t()), hub_secret_names: list(String.t()),
file_entries: list(file_entry()), file_entries: list(file_entry()),
quarantine_file_entry_names: MapSet.new(String.t()), quarantine_file_entry_names: MapSet.new(String.t()),
teams_enabled: boolean() teams_enabled: boolean(),
deployment_group_id: String.t() | nil
} }
@typedoc """ @typedoc """
@ -110,7 +112,8 @@ defmodule Livebook.Notebook do
hub_secret_names: [], hub_secret_names: [],
file_entries: [], file_entries: [],
quarantine_file_entry_names: MapSet.new(), quarantine_file_entry_names: MapSet.new(),
teams_enabled: false teams_enabled: false,
deployment_group_id: nil
} }
|> put_setup_cell(Cell.new(:code)) |> put_setup_cell(Cell.new(:code))
end end

View file

@ -606,6 +606,14 @@ defmodule Livebook.Session do
GenServer.cast(pid, {:set_notebook_hub, self(), id}) GenServer.cast(pid, {:set_notebook_hub, self(), id})
end end
@doc """
Sends a deployment group selection request to the server.
"""
@spec set_notebook_deployment_group(pid(), String.t()) :: :ok
def set_notebook_deployment_group(pid, id) do
GenServer.cast(pid, {:set_notebook_deployment_group, self(), id})
end
@doc """ @doc """
Sends a file entries addition request to the server. Sends a file entries addition request to the server.
@ -1393,6 +1401,12 @@ defmodule Livebook.Session do
{:noreply, handle_operation(state, operation)} {:noreply, handle_operation(state, operation)}
end end
def handle_cast({:set_notebook_deployment_group, client_pid, id}, state) do
client_id = client_id(state, client_pid)
operation = {:set_notebook_deployment_group, client_id, id}
{:noreply, handle_operation(state, operation)}
end
def handle_cast({:add_file_entries, client_pid, file_entries}, state) do def handle_cast({:add_file_entries, client_pid, file_entries}, state) do
client_id = client_id(state, client_pid) client_id = client_id(state, client_pid)
operation = {:add_file_entries, client_id, file_entries} operation = {:add_file_entries, client_id, file_entries}

View file

@ -227,6 +227,7 @@ defmodule Livebook.Session.Data do
| {:set_deployed_app_slug, client_id(), String.t()} | {:set_deployed_app_slug, client_id(), String.t()}
| {:app_deactivate, client_id()} | {:app_deactivate, client_id()}
| {:app_shutdown, client_id()} | {:app_shutdown, client_id()}
| {:set_notebook_deployment_group, String.t()}
@type action :: @type action ::
:connect_runtime :connect_runtime
@ -905,11 +906,20 @@ defmodule Livebook.Session.Data do
|> with_actions() |> with_actions()
|> set_notebook_hub(hub) |> set_notebook_hub(hub)
|> update_notebook_hub_secret_names() |> update_notebook_hub_secret_names()
|> set_notebook_deployment_group(nil)
|> set_dirty() |> set_dirty()
|> wrap_ok() |> wrap_ok()
end end
end end
def apply_operation(data, {:set_notebook_deployment_group, _client_id, id}) do
data
|> with_actions()
|> set_notebook_deployment_group(id)
|> set_dirty()
|> wrap_ok()
end
def apply_operation(data, {:sync_hub_secrets, _client_id}) do def apply_operation(data, {:sync_hub_secrets, _client_id}) do
data data
|> with_actions() |> with_actions()
@ -1714,6 +1724,10 @@ defmodule Livebook.Session.Data do
) )
end end
defp set_notebook_deployment_group({data, _} = data_actions, id) do
set!(data_actions, notebook: %{data.notebook | deployment_group_id: id})
end
defp sync_hub_secrets({data, _} = data_actions) do defp sync_hub_secrets({data, _} = data_actions) do
hub = Livebook.Hubs.fetch_hub!(data.notebook.hub_id) hub = Livebook.Hubs.fetch_hub!(data.notebook.hub_id)
secrets = Livebook.Hubs.get_secrets(hub) secrets = Livebook.Hubs.get_secrets(hub)

View file

@ -502,6 +502,7 @@ defmodule LivebookWeb.SessionLive do
secrets={@data_view.secrets} secrets={@data_view.secrets}
file_entries={@data_view.file_entries} file_entries={@data_view.file_entries}
settings={@data_view.app_settings} settings={@data_view.app_settings}
deployment_group_id={@data_view.deployment_group_id}
/> />
</.modal> </.modal>
@ -2671,7 +2672,8 @@ defmodule LivebookWeb.SessionLive do
file_entries: Enum.sort_by(data.notebook.file_entries, & &1.name), file_entries: Enum.sort_by(data.notebook.file_entries, & &1.name),
quarantine_file_entry_names: data.notebook.quarantine_file_entry_names, quarantine_file_entry_names: data.notebook.quarantine_file_entry_names,
app_settings: data.notebook.app_settings, app_settings: data.notebook.app_settings,
deployed_app_slug: data.deployed_app_slug deployed_app_slug: data.deployed_app_slug,
deployment_group_id: data.notebook.deployment_group_id
} }
end end

View file

@ -6,17 +6,21 @@ defmodule LivebookWeb.SessionLive.AppDockerComponent do
alias Livebook.Hubs alias Livebook.Hubs
alias Livebook.FileSystem alias Livebook.FileSystem
alias LivebookWeb.AppHelpers alias LivebookWeb.AppHelpers
alias Livebook.Hubs.Provider
@impl true @impl true
def update(assigns, socket) do def update(assigns, socket) do
socket = assign(socket, assigns) socket = assign(socket, assigns)
deployment_groups = Provider.deployment_groups(assigns.hub)
{:ok, {:ok,
socket socket
|> assign(settings_valid?: Livebook.Notebook.AppSettings.valid?(socket.assigns.settings)) |> assign(settings_valid?: Livebook.Notebook.AppSettings.valid?(socket.assigns.settings))
|> assign( |> assign(
hub_secrets: Hubs.get_secrets(assigns.hub), hub_secrets: Hubs.get_secrets(assigns.hub),
hub_file_systems: Hubs.get_file_systems(assigns.hub, hub_only: true) hub_file_systems: Hubs.get_file_systems(assigns.hub, hub_only: true),
deployment_groups: deployment_groups,
deployment_group_form: %{"id" => assigns.deployment_group_id}
) )
|> assign_new(:changeset, fn -> Hubs.Dockerfile.config_changeset() end) |> assign_new(:changeset, fn -> Hubs.Dockerfile.config_changeset() end)
|> assign_new(:save_result, fn -> nil end) |> assign_new(:save_result, fn -> nil end)
@ -34,6 +38,8 @@ defmodule LivebookWeb.SessionLive.AppDockerComponent do
file={@file} file={@file}
settings_valid?={@settings_valid?} settings_valid?={@settings_valid?}
hub={@hub} hub={@hub}
deployment_groups={@deployment_groups}
deployment_group_form={@deployment_group_form}
changeset={@changeset} changeset={@changeset}
session={@session} session={@session}
dockerfile={@dockerfile} dockerfile={@dockerfile}
@ -80,13 +86,42 @@ defmodule LivebookWeb.SessionLive.AppDockerComponent do
You can deploy this app in the cloud using Docker. To do that, configure You can deploy this app in the cloud using Docker. To do that, configure
the deployment and then use the generated Dockerfile. the deployment and then use the generated Dockerfile.
</p> </p>
<p class="text-gray-700"> <div class="flex gap-12">
<.label>Hub</.label> <p class="text-gray-700">
<span> <.label>Hub</.label>
<span class="text-lg"><%= @hub.hub_emoji %></span> <span>
<span><%= @hub.hub_name %></span> <span class="text-lg"><%= @hub.hub_emoji %></span>
</span> <span><%= @hub.hub_name %></span>
</p> </span>
</p>
<%= if @deployment_groups do %>
<%= if @deployment_groups != [] do %>
<.form
:let={f}
for={@deployment_group_form}
phx-change="select_deployment_group"
phx-target={@myself}
id="select_deployment_group_form"
>
<.select_field
help={
~S'''
Share deployment credentials, secrets, and configuration with deployment groups.
'''
}
field={f[:id]}
options={deployment_group_options(@deployment_groups)}
label="Deployment Group"
/>
</.form>
<% else %>
<p class="text-gray-700">
<.label>Deployment Group</.label>
<span>No deployment groups available</span>
</p>
<% end %>
<% end %>
</div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<.message_box :for={warning <- @warnings} kind={:warning}> <.message_box :for={warning <- @warnings} kind={:warning}>
<%= raw(warning) %> <%= raw(warning) %>
@ -156,6 +191,12 @@ defmodule LivebookWeb.SessionLive.AppDockerComponent do
{:noreply, assign(socket, save_result: save_result)} {:noreply, assign(socket, save_result: save_result)}
end end
def handle_event("select_deployment_group", %{"id" => id}, socket) do
Livebook.Session.set_notebook_deployment_group(socket.assigns.session.pid, id)
{:noreply, socket}
end
defp update_dockerfile(socket) when socket.assigns.file == nil do defp update_dockerfile(socket) when socket.assigns.file == nil do
assign(socket, dockerfile: nil, warnings: []) assign(socket, dockerfile: nil, warnings: [])
end end
@ -170,9 +211,21 @@ defmodule LivebookWeb.SessionLive.AppDockerComponent do
file: file, file: file,
file_entries: file_entries, file_entries: file_entries,
secrets: secrets, secrets: secrets,
app_settings: app_settings app_settings: app_settings,
deployment_groups: deployment_groups,
deployment_group_id: deployment_group_id
} = socket.assigns } = socket.assigns
deployment_group =
if deployment_group_id, do: Enum.find(deployment_groups, &(&1.id == deployment_group_id))
hub_secrets =
if deployment_group do
Enum.uniq_by(deployment_group.secrets ++ hub_secrets, & &1.name)
else
hub_secrets
end
dockerfile = dockerfile =
Hubs.Dockerfile.build_dockerfile( Hubs.Dockerfile.build_dockerfile(
config, config,
@ -197,4 +250,9 @@ defmodule LivebookWeb.SessionLive.AppDockerComponent do
assign(socket, dockerfile: dockerfile, warnings: warnings) assign(socket, dockerfile: dockerfile, warnings: warnings)
end end
defp deployment_group_options(deployment_groups) do
for deployment_group <- [%{name: "none", id: nil}] ++ deployment_groups,
do: {deployment_group.name, deployment_group.id}
end
end end

View file

@ -359,4 +359,103 @@ defmodule LivebookWeb.Integration.SessionLiveTest do
remove_offline_hub_file_system(file_system) remove_offline_hub_file_system(file_system)
end end
end end
describe "deployment group for app deployment" do
@tag :tmp_dir
test "show deployment group on app deployment",
%{conn: conn, user: user, node: node, session: session, tmp_dir: tmp_dir} do
team = create_team_hub(user, node)
team_id = team.id
insert_deployment_group(
name: "DEPLOYMENT_GROUP_SUSIE",
mode: "online",
hub_id: team_id
)
Session.subscribe(session.id)
Session.set_notebook_hub(session.pid, team_id)
assert_receive {:operation, {:set_notebook_hub, _client, ^team_id}}
notebook_path = Path.join(tmp_dir, "notebook.livemd")
file = Livebook.FileSystem.File.local(notebook_path)
Session.set_file(session.pid, file)
slug = Livebook.Utils.random_short_id()
app_settings = %{Livebook.Notebook.AppSettings.new() | slug: slug}
Session.set_app_settings(session.pid, app_settings)
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}/app-docker")
assert render(view) =~ "Deployment Group"
assert has_element?(view, "#select_deployment_group_form")
end
@tag :tmp_dir
test "set deployment group on app deployment",
%{conn: conn, user: user, node: node, session: session, tmp_dir: tmp_dir} do
team = create_team_hub(user, node)
team_id = team.id
insert_deployment_group(
name: "DEPLOYMENT_GROUP_SUSIE",
mode: "online",
hub_id: team_id
)
insert_deployment_group(
name: "DEPLOYMENT_GROUP_TOBIAS",
mode: "online",
hub_id: team_id
)
Session.subscribe(session.id)
Session.set_notebook_hub(session.pid, team_id)
assert_receive {:operation, {:set_notebook_hub, _client, ^team_id}}
notebook_path = Path.join(tmp_dir, "notebook.livemd")
file = Livebook.FileSystem.File.local(notebook_path)
Session.set_file(session.pid, file)
slug = Livebook.Utils.random_short_id()
app_settings = %{Livebook.Notebook.AppSettings.new() | slug: slug}
Session.set_app_settings(session.pid, app_settings)
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}/app-docker")
assert render(view) =~ "Deployment Group"
assert has_element?(view, "#select_deployment_group_form")
view
|> form("#select_deployment_group_form", %{id: "2"})
|> render_change()
assert_receive {:operation, {:set_notebook_deployment_group, _client, "2"}}
end
@tag :tmp_dir
test "show no deployments groups available",
%{conn: conn, user: user, node: node, session: session, tmp_dir: tmp_dir} do
team = create_team_hub(user, node)
team_id = team.id
Session.subscribe(session.id)
Session.set_notebook_hub(session.pid, team_id)
assert_receive {:operation, {:set_notebook_hub, _client, ^team_id}}
notebook_path = Path.join(tmp_dir, "notebook.livemd")
file = Livebook.FileSystem.File.local(notebook_path)
Session.set_file(session.pid, file)
slug = Livebook.Utils.random_short_id()
app_settings = %{Livebook.Notebook.AppSettings.new() | slug: slug}
Session.set_app_settings(session.pid, app_settings)
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}/app-docker")
assert render(view) =~ "Deployment Group"
assert render(view) =~ "No deployment groups available"
refute has_element?(view, "#select_deployment_group_form")
end
end
end end