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 = Broadcasts.file_system_deleted(file_system)
end
def deployment_groups(_personal), do: nil
end

View file

@ -138,4 +138,13 @@ defprotocol Livebook.Hubs.Provider do
"""
@spec delete_file_system(t(), FileSystem.t()) :: :ok | {:transport_error, String.t()}
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

View file

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

View file

@ -102,7 +102,14 @@ defmodule Livebook.LiveMarkdown.Export do
end
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))
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} ->
{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 =
Map.merge(

View file

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

View file

@ -606,6 +606,14 @@ defmodule Livebook.Session do
GenServer.cast(pid, {:set_notebook_hub, self(), id})
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 """
Sends a file entries addition request to the server.
@ -1393,6 +1401,12 @@ defmodule Livebook.Session do
{:noreply, handle_operation(state, operation)}
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
client_id = client_id(state, client_pid)
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()}
| {:app_deactivate, client_id()}
| {:app_shutdown, client_id()}
| {:set_notebook_deployment_group, String.t()}
@type action ::
:connect_runtime
@ -905,11 +906,20 @@ defmodule Livebook.Session.Data do
|> with_actions()
|> set_notebook_hub(hub)
|> update_notebook_hub_secret_names()
|> set_notebook_deployment_group(nil)
|> set_dirty()
|> wrap_ok()
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
data
|> with_actions()
@ -1714,6 +1724,10 @@ defmodule Livebook.Session.Data do
)
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
hub = Livebook.Hubs.fetch_hub!(data.notebook.hub_id)
secrets = Livebook.Hubs.get_secrets(hub)

View file

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

View file

@ -6,17 +6,21 @@ defmodule LivebookWeb.SessionLive.AppDockerComponent do
alias Livebook.Hubs
alias Livebook.FileSystem
alias LivebookWeb.AppHelpers
alias Livebook.Hubs.Provider
@impl true
def update(assigns, socket) do
socket = assign(socket, assigns)
deployment_groups = Provider.deployment_groups(assigns.hub)
{:ok,
socket
|> assign(settings_valid?: Livebook.Notebook.AppSettings.valid?(socket.assigns.settings))
|> assign(
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(:save_result, fn -> nil end)
@ -34,6 +38,8 @@ defmodule LivebookWeb.SessionLive.AppDockerComponent do
file={@file}
settings_valid?={@settings_valid?}
hub={@hub}
deployment_groups={@deployment_groups}
deployment_group_form={@deployment_group_form}
changeset={@changeset}
session={@session}
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
the deployment and then use the generated Dockerfile.
</p>
<p class="text-gray-700">
<.label>Hub</.label>
<span>
<span class="text-lg"><%= @hub.hub_emoji %></span>
<span><%= @hub.hub_name %></span>
</span>
</p>
<div class="flex gap-12">
<p class="text-gray-700">
<.label>Hub</.label>
<span>
<span class="text-lg"><%= @hub.hub_emoji %></span>
<span><%= @hub.hub_name %></span>
</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">
<.message_box :for={warning <- @warnings} kind={:warning}>
<%= raw(warning) %>
@ -156,6 +191,12 @@ defmodule LivebookWeb.SessionLive.AppDockerComponent do
{:noreply, assign(socket, save_result: save_result)}
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
assign(socket, dockerfile: nil, warnings: [])
end
@ -170,9 +211,21 @@ defmodule LivebookWeb.SessionLive.AppDockerComponent do
file: file,
file_entries: file_entries,
secrets: secrets,
app_settings: app_settings
app_settings: app_settings,
deployment_groups: deployment_groups,
deployment_group_id: deployment_group_id
} = 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 =
Hubs.Dockerfile.build_dockerfile(
config,
@ -197,4 +250,9 @@ defmodule LivebookWeb.SessionLive.AppDockerComponent do
assign(socket, dockerfile: dockerfile, warnings: warnings)
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

View file

@ -359,4 +359,103 @@ defmodule LivebookWeb.Integration.SessionLiveTest do
remove_offline_hub_file_system(file_system)
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