mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-10-09 04:57:18 +08:00
Deploy apps to Livebook Agent (#2511)
This commit is contained in:
parent
40a4fda509
commit
ddc2ad0a85
14 changed files with 971 additions and 55 deletions
|
@ -279,6 +279,21 @@ defmodule Livebook.Hubs.TeamClient do
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp put_app_deployment(deployment_group, app_deployment) do
|
||||||
|
deployment_group = remove_app_deployment(deployment_group, app_deployment)
|
||||||
|
app_deployments = [app_deployment | deployment_group.app_deployments]
|
||||||
|
|
||||||
|
%{deployment_group | app_deployments: Enum.sort_by(app_deployments, & &1.slug)}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp remove_app_deployment(deployment_group, app_deployment) do
|
||||||
|
%{
|
||||||
|
deployment_group
|
||||||
|
| app_deployments:
|
||||||
|
Enum.reject(deployment_group.app_deployments, &(&1.slug == app_deployment.slug))
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
defp build_agent_key(agent_key) do
|
defp build_agent_key(agent_key) do
|
||||||
%Teams.AgentKey{
|
%Teams.AgentKey{
|
||||||
id: agent_key.id,
|
id: agent_key.id,
|
||||||
|
@ -290,6 +305,7 @@ defmodule Livebook.Hubs.TeamClient do
|
||||||
defp build_deployment_group(state, deployment_group) do
|
defp build_deployment_group(state, deployment_group) do
|
||||||
secrets = Enum.map(deployment_group.secrets, &build_secret(state, &1))
|
secrets = Enum.map(deployment_group.secrets, &build_secret(state, &1))
|
||||||
agent_keys = Enum.map(deployment_group.agent_keys, &build_agent_key/1)
|
agent_keys = Enum.map(deployment_group.agent_keys, &build_agent_key/1)
|
||||||
|
app_deployments = Enum.map(deployment_group.deployed_apps, &build_app_deployment/1)
|
||||||
|
|
||||||
%Teams.DeploymentGroup{
|
%Teams.DeploymentGroup{
|
||||||
id: deployment_group.id,
|
id: deployment_group.id,
|
||||||
|
@ -298,12 +314,29 @@ defmodule Livebook.Hubs.TeamClient do
|
||||||
hub_id: state.hub.id,
|
hub_id: state.hub.id,
|
||||||
secrets: secrets,
|
secrets: secrets,
|
||||||
agent_keys: agent_keys,
|
agent_keys: agent_keys,
|
||||||
|
app_deployments: app_deployments,
|
||||||
clustering: nullify(deployment_group.clustering),
|
clustering: nullify(deployment_group.clustering),
|
||||||
zta_provider: atomize(deployment_group.zta_provider),
|
zta_provider: atomize(deployment_group.zta_provider),
|
||||||
zta_key: nullify(deployment_group.zta_key)
|
zta_key: nullify(deployment_group.zta_key)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp build_app_deployment(app_deployment) do
|
||||||
|
path = URI.parse(app_deployment.archive_url).path
|
||||||
|
|
||||||
|
%Teams.AppDeployment{
|
||||||
|
id: app_deployment.id,
|
||||||
|
filename: Path.basename(path),
|
||||||
|
slug: app_deployment.slug,
|
||||||
|
sha: app_deployment.sha,
|
||||||
|
title: app_deployment.title,
|
||||||
|
deployment_group_id: app_deployment.deployment_group_id,
|
||||||
|
file: {:url, app_deployment.archive_url},
|
||||||
|
deployed_by: app_deployment.deployed_by,
|
||||||
|
deployed_at: NaiveDateTime.from_iso8601!(app_deployment.deployed_at)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
defp handle_event(:secret_created, %Secrets.Secret{} = secret, state) do
|
defp handle_event(:secret_created, %Secrets.Secret{} = secret, state) do
|
||||||
Hubs.Broadcasts.secret_created(secret)
|
Hubs.Broadcasts.secret_created(secret)
|
||||||
|
|
||||||
|
@ -382,6 +415,7 @@ defmodule Livebook.Hubs.TeamClient do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp handle_event(:deployment_group_updated, %Teams.DeploymentGroup{} = deployment_group, state) do
|
defp handle_event(:deployment_group_updated, %Teams.DeploymentGroup{} = deployment_group, state) do
|
||||||
|
deployment_group = deploy_apps(state.deployment_group_id, deployment_group, state.derived_key)
|
||||||
Teams.Broadcasts.deployment_group_updated(deployment_group)
|
Teams.Broadcasts.deployment_group_updated(deployment_group)
|
||||||
|
|
||||||
put_deployment_group(state, deployment_group)
|
put_deployment_group(state, deployment_group)
|
||||||
|
@ -397,6 +431,7 @@ defmodule Livebook.Hubs.TeamClient do
|
||||||
|
|
||||||
defp handle_event(:deployment_group_deleted, deployment_group_deleted, state) do
|
defp handle_event(:deployment_group_deleted, deployment_group_deleted, state) do
|
||||||
with {:ok, deployment_group} <- fetch_deployment_group(deployment_group_deleted.id, state) do
|
with {:ok, deployment_group} <- fetch_deployment_group(deployment_group_deleted.id, state) do
|
||||||
|
undeploy_apps(state.deployment_group_id, deployment_group)
|
||||||
Teams.Broadcasts.deployment_group_deleted(deployment_group)
|
Teams.Broadcasts.deployment_group_deleted(deployment_group)
|
||||||
|
|
||||||
remove_deployment_group(state, deployment_group)
|
remove_deployment_group(state, deployment_group)
|
||||||
|
@ -436,6 +471,24 @@ defmodule Livebook.Hubs.TeamClient do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp handle_event(:app_deployment_created, app_deployment_created, state) do
|
||||||
|
app_deployment = build_app_deployment(app_deployment_created)
|
||||||
|
deployment_group_id = app_deployment.deployment_group_id
|
||||||
|
|
||||||
|
with {:ok, deployment_group} <- fetch_deployment_group(deployment_group_id, state) do
|
||||||
|
app_deployment =
|
||||||
|
if deployment_group_id == state.deployment_group_id do
|
||||||
|
{:ok, app_deployment} = download_and_deploy(app_deployment, state.derived_key)
|
||||||
|
app_deployment
|
||||||
|
else
|
||||||
|
app_deployment
|
||||||
|
end
|
||||||
|
|
||||||
|
deployment_group = put_app_deployment(deployment_group, app_deployment)
|
||||||
|
handle_event(:deployment_group_updated, deployment_group, state)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp dispatch_secrets(state, %{secrets: secrets}) do
|
defp dispatch_secrets(state, %{secrets: secrets}) do
|
||||||
decrypted_secrets = Enum.map(secrets, &build_secret(state, &1))
|
decrypted_secrets = Enum.map(secrets, &build_secret(state, &1))
|
||||||
|
|
||||||
|
@ -534,4 +587,59 @@ defmodule Livebook.Hubs.TeamClient do
|
||||||
|
|
||||||
defp nullify(""), do: nil
|
defp nullify(""), do: nil
|
||||||
defp nullify(value), do: value
|
defp nullify(value), do: value
|
||||||
|
|
||||||
|
defp deploy_apps(id, %{id: id} = deployment_group, derived_key) do
|
||||||
|
app_deployments =
|
||||||
|
for app_deployment <- deployment_group.app_deployments do
|
||||||
|
{:ok, app_deployment} = download_and_deploy(app_deployment, derived_key)
|
||||||
|
app_deployment
|
||||||
|
end
|
||||||
|
|
||||||
|
%{deployment_group | app_deployments: app_deployments}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp deploy_apps(_, deployment_group, _), do: deployment_group
|
||||||
|
|
||||||
|
defp undeploy_apps(id, %{id: id} = deployment_group) do
|
||||||
|
for %{slug: slug} <- deployment_group.app_deployments do
|
||||||
|
:ok = undeploy_app(slug)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp undeploy_apps(_, _), do: :noop
|
||||||
|
|
||||||
|
defp download_and_deploy(%{file: nil} = app_deployment, _) do
|
||||||
|
app_deployment
|
||||||
|
end
|
||||||
|
|
||||||
|
defp download_and_deploy(%{file: {:url, archive_url}} = app_deployment, derived_key) do
|
||||||
|
destination_path = app_deployment_path(app_deployment.slug)
|
||||||
|
|
||||||
|
with {:ok, %{status: 200} = response} <- Req.get(archive_url),
|
||||||
|
:ok <- undeploy_app(app_deployment.slug),
|
||||||
|
{:ok, decrypted_content} <- Teams.decrypt(response.body, derived_key),
|
||||||
|
:ok <- unzip_app(decrypted_content, destination_path),
|
||||||
|
:ok <- Livebook.Apps.deploy_apps_in_dir(destination_path) do
|
||||||
|
{:ok, %{app_deployment | file: nil}}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp undeploy_app(slug) do
|
||||||
|
with {:ok, app} <- Livebook.Apps.fetch_app(slug) do
|
||||||
|
Livebook.App.close(app.pid)
|
||||||
|
end
|
||||||
|
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
defp app_deployment_path(slug) do
|
||||||
|
Path.join([Livebook.Config.tmp_path(), "apps", slug <> Livebook.Utils.random_short_id()])
|
||||||
|
end
|
||||||
|
|
||||||
|
defp unzip_app(content, destination_path) do
|
||||||
|
case :zip.extract(content, cwd: to_charlist(destination_path)) do
|
||||||
|
{:ok, _} -> :ok
|
||||||
|
{:error, error} -> FileSystem.Utils.posix_error(error)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,7 +4,7 @@ defmodule Livebook.Teams do
|
||||||
alias Livebook.Hubs
|
alias Livebook.Hubs
|
||||||
alias Livebook.Hubs.Team
|
alias Livebook.Hubs.Team
|
||||||
alias Livebook.Hubs.TeamClient
|
alias Livebook.Hubs.TeamClient
|
||||||
alias Livebook.Teams.{AgentKey, DeploymentGroup, Org, Requests}
|
alias Livebook.Teams.{AgentKey, AppDeployment, DeploymentGroup, Org, Requests}
|
||||||
|
|
||||||
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]
|
||||||
|
@ -249,4 +249,27 @@ defmodule Livebook.Teams do
|
||||||
[]
|
[]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Creates a new app deployment.
|
||||||
|
"""
|
||||||
|
@spec deploy_app(Team.t(), AppDeployment.t()) ::
|
||||||
|
:ok
|
||||||
|
| {:error, Ecto.Changeset.t()}
|
||||||
|
| {:transport_error, String.t()}
|
||||||
|
def deploy_app(%Team{} = team, %AppDeployment{} = app_deployment) do
|
||||||
|
case Requests.deploy_app(team, app_deployment) do
|
||||||
|
{:ok, %{"id" => _id}} ->
|
||||||
|
:ok
|
||||||
|
|
||||||
|
{:error, %{"errors" => %{"detail" => error}}} ->
|
||||||
|
{:error, Requests.add_errors(app_deployment, %{"file" => [error]})}
|
||||||
|
|
||||||
|
{:error, %{"errors" => errors}} ->
|
||||||
|
{:error, Requests.add_errors(app_deployment, errors)}
|
||||||
|
|
||||||
|
any ->
|
||||||
|
any
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
88
lib/livebook/teams/app_deployment.ex
Normal file
88
lib/livebook/teams/app_deployment.ex
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
defmodule Livebook.Teams.AppDeployment do
|
||||||
|
use Ecto.Schema
|
||||||
|
alias Livebook.FileSystem
|
||||||
|
|
||||||
|
@file_extension ".zip"
|
||||||
|
@max_size 20 * 1024 * 1024
|
||||||
|
|
||||||
|
@type t :: %__MODULE__{
|
||||||
|
id: String.t() | nil,
|
||||||
|
filename: String.t() | nil,
|
||||||
|
slug: String.t() | nil,
|
||||||
|
sha: String.t() | nil,
|
||||||
|
title: String.t() | nil,
|
||||||
|
deployment_group_id: String.t() | nil,
|
||||||
|
file: {:url, String.t()} | {:content, binary()} | nil,
|
||||||
|
deployed_by: String.t() | nil,
|
||||||
|
deployed_at: NaiveDateTime.t() | nil
|
||||||
|
}
|
||||||
|
|
||||||
|
@primary_key {:id, :string, autogenerate: false}
|
||||||
|
embedded_schema do
|
||||||
|
field :filename, :string
|
||||||
|
field :slug, :string
|
||||||
|
field :sha, :string
|
||||||
|
field :title, :string
|
||||||
|
field :deployment_group_id, :string
|
||||||
|
field :file, :string
|
||||||
|
field :deployed_by, :string
|
||||||
|
|
||||||
|
timestamps(updated_at: nil, inserted_at: :deployed_at)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Creates a new app deployment from notebook.
|
||||||
|
"""
|
||||||
|
@spec new(Livebook.Notebook.t(), Livebook.FileSystem.File.t()) ::
|
||||||
|
{:ok, t()} | {:warning, list(String.t())} | {:error, FileSystem.error()}
|
||||||
|
def new(notebook, files_dir) do
|
||||||
|
with {:ok, source} <- fetch_notebook_source(notebook),
|
||||||
|
{:ok, files} <- build_and_check_file_entries(notebook, source, files_dir),
|
||||||
|
{:ok, {_, zip_content}} <- :zip.create(~c"app_deployment.zip", files, [:memory]),
|
||||||
|
:ok <- validate_size(zip_content) do
|
||||||
|
md5_hash = :crypto.hash(:md5, zip_content)
|
||||||
|
shasum = Base.encode16(md5_hash, case: :lower)
|
||||||
|
|
||||||
|
{:ok,
|
||||||
|
%__MODULE__{
|
||||||
|
filename: shasum <> @file_extension,
|
||||||
|
slug: notebook.app_settings.slug,
|
||||||
|
sha: shasum,
|
||||||
|
title: notebook.name,
|
||||||
|
deployment_group_id: notebook.deployment_group_id,
|
||||||
|
file: {:content, zip_content}
|
||||||
|
}}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp fetch_notebook_source(notebook) do
|
||||||
|
case Livebook.LiveMarkdown.notebook_to_livemd(notebook) do
|
||||||
|
{source, []} -> {:ok, source}
|
||||||
|
{_, warnings} -> {:warning, warnings}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp build_and_check_file_entries(notebook, source, files_dir) do
|
||||||
|
notebook.file_entries
|
||||||
|
|> Enum.filter(&(&1.type == :attachment))
|
||||||
|
|> Enum.reduce_while({:ok, [{~c"notebook.livemd", source}]}, fn %{name: name}, {:ok, acc} ->
|
||||||
|
file = Livebook.FileSystem.File.resolve(files_dir, name)
|
||||||
|
|
||||||
|
with {:ok, true} <- Livebook.FileSystem.File.exists?(file),
|
||||||
|
{:ok, content} <- Livebook.FileSystem.File.read(file) do
|
||||||
|
{:cont, {:ok, [{to_charlist("files/" <> name), content} | acc]}}
|
||||||
|
else
|
||||||
|
{:ok, false} -> {:halt, {:error, "files/#{name}: doesn't exist"}}
|
||||||
|
{:error, reason} -> {:halt, {:error, "files/#{name}: #{reason}"}}
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp validate_size(data) do
|
||||||
|
if byte_size(data) <= @max_size do
|
||||||
|
:ok
|
||||||
|
else
|
||||||
|
{:error, "the notebook and its attachments have exceeded the maximum size of 20MB"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -3,7 +3,7 @@ defmodule Livebook.Teams.DeploymentGroup do
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
|
|
||||||
alias Livebook.Secrets.Secret
|
alias Livebook.Secrets.Secret
|
||||||
alias Livebook.Teams.AgentKey
|
alias Livebook.Teams.{AgentKey, AppDeployment}
|
||||||
|
|
||||||
# If this list is updated, it must also be mirrored on Livebook Teams Server.
|
# If this list is updated, it must also be mirrored on Livebook Teams Server.
|
||||||
@zta_providers ~w(cloudflare google_iap tailscale teleport)a
|
@zta_providers ~w(cloudflare google_iap tailscale teleport)a
|
||||||
|
@ -17,7 +17,8 @@ defmodule Livebook.Teams.DeploymentGroup do
|
||||||
zta_provider: :cloudflare | :google_iap | :tailscale | :teleport,
|
zta_provider: :cloudflare | :google_iap | :tailscale | :teleport,
|
||||||
zta_key: String.t(),
|
zta_key: String.t(),
|
||||||
secrets: [Secret.t()],
|
secrets: [Secret.t()],
|
||||||
agent_keys: [AgentKey.t()]
|
agent_keys: [AgentKey.t()],
|
||||||
|
app_deployments: [AppDeployment.t()]
|
||||||
}
|
}
|
||||||
|
|
||||||
@primary_key {:id, :string, autogenerate: false}
|
@primary_key {:id, :string, autogenerate: false}
|
||||||
|
@ -31,6 +32,7 @@ defmodule Livebook.Teams.DeploymentGroup do
|
||||||
|
|
||||||
has_many :secrets, Secret
|
has_many :secrets, Secret
|
||||||
has_many :agent_keys, AgentKey
|
has_many :agent_keys, AgentKey
|
||||||
|
has_many :app_deployments, AppDeployment
|
||||||
end
|
end
|
||||||
|
|
||||||
def changeset(deployment_group, attrs \\ %{}) do
|
def changeset(deployment_group, attrs \\ %{}) do
|
||||||
|
|
|
@ -4,7 +4,9 @@ defmodule Livebook.Teams.Requests do
|
||||||
alias Livebook.Hubs.Team
|
alias Livebook.Hubs.Team
|
||||||
alias Livebook.Secrets.Secret
|
alias Livebook.Secrets.Secret
|
||||||
alias Livebook.Teams
|
alias Livebook.Teams
|
||||||
alias Livebook.Teams.{AgentKey, DeploymentGroup, Org}
|
alias Livebook.Teams.{AgentKey, AppDeployment, DeploymentGroup, Org}
|
||||||
|
|
||||||
|
@error_message "Something went wrong, try again later or please file a bug if it persists"
|
||||||
|
|
||||||
@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.
|
||||||
|
@ -216,6 +218,26 @@ defmodule Livebook.Teams.Requests do
|
||||||
delete("/api/v1/org/deployment-groups/agent-keys", params, team)
|
delete("/api/v1/org/deployment-groups/agent-keys", params, team)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Send a request to Livebook Team API to deploy an app.
|
||||||
|
"""
|
||||||
|
@spec deploy_app(Team.t(), AppDeployment.t()) ::
|
||||||
|
{:ok, map()} | {:error, map() | String.t()} | {:transport_error, String.t()}
|
||||||
|
def deploy_app(team, %{file: {:content, content}} = app_deployment) do
|
||||||
|
secret_key = Teams.derive_key(team.teams_key)
|
||||||
|
|
||||||
|
params = %{
|
||||||
|
filename: app_deployment.filename <> ".encrypted",
|
||||||
|
title: app_deployment.title,
|
||||||
|
slug: app_deployment.slug,
|
||||||
|
deployment_group_id: app_deployment.deployment_group_id,
|
||||||
|
sha: app_deployment.sha
|
||||||
|
}
|
||||||
|
|
||||||
|
encrypted_content = Teams.encrypt(content, secret_key)
|
||||||
|
upload("/api/v1/org/apps", encrypted_content, params, team)
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Add requests errors to a `changeset` for the given `fields`.
|
Add requests errors to a `changeset` for the given `fields`.
|
||||||
"""
|
"""
|
||||||
|
@ -235,6 +257,9 @@ defmodule Livebook.Teams.Requests do
|
||||||
value |> Ecto.Changeset.change() |> add_errors(struct.__schema__(:fields), errors_map)
|
value |> Ecto.Changeset.change() |> add_errors(struct.__schema__(:fields), errors_map)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
def error_message(), do: @error_message
|
||||||
|
|
||||||
defp post(path, json, team \\ nil) do
|
defp post(path, json, team \\ nil) do
|
||||||
build_req()
|
build_req()
|
||||||
|> add_team_auth(team)
|
|> add_team_auth(team)
|
||||||
|
@ -261,6 +286,14 @@ defmodule Livebook.Teams.Requests do
|
||||||
|> request(method: :get, url: path, params: params)
|
|> request(method: :get, url: path, params: params)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp upload(path, content, params, team) do
|
||||||
|
build_req()
|
||||||
|
|> add_team_auth(team)
|
||||||
|
|> Req.Request.put_header("content-length", "#{byte_size(content)}")
|
||||||
|
|> request(method: :post, url: path, params: params, body: content)
|
||||||
|
|> dispatch_messages(team)
|
||||||
|
end
|
||||||
|
|
||||||
defp build_req() do
|
defp build_req() do
|
||||||
Req.new(
|
Req.new(
|
||||||
base_url: Livebook.Config.teams_url(),
|
base_url: Livebook.Config.teams_url(),
|
||||||
|
@ -301,8 +334,7 @@ defmodule Livebook.Teams.Requests do
|
||||||
"You are not authorized to perform this action, make sure you have the access or you are not in a Livebook Agent instance"}
|
"You are not authorized to perform this action, make sure you have the access or you are not in a Livebook Agent instance"}
|
||||||
|
|
||||||
_otherwise ->
|
_otherwise ->
|
||||||
{:transport_error,
|
{:transport_error, @error_message}
|
||||||
"Something went wrong, try again later or please file a bug if it persists"}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,11 @@ defmodule LivebookWeb.Hub.Teams.DeploymentGroupLive do
|
||||||
do: deployment_group.agent_keys,
|
do: deployment_group.agent_keys,
|
||||||
else: []
|
else: []
|
||||||
|
|
||||||
|
app_deployments =
|
||||||
|
if socket.assigns.live_action != :new_deployment_group,
|
||||||
|
do: deployment_group.app_deployments,
|
||||||
|
else: []
|
||||||
|
|
||||||
secret_value =
|
secret_value =
|
||||||
if socket.assigns.live_action == :edit_secret do
|
if socket.assigns.live_action == :edit_secret do
|
||||||
Enum.find_value(secrets, &(&1.name == secret_name and &1.value)) ||
|
Enum.find_value(secrets, &(&1.name == secret_name and &1.value)) ||
|
||||||
|
@ -55,7 +60,8 @@ defmodule LivebookWeb.Hub.Teams.DeploymentGroupLive do
|
||||||
secret_value: secret_value,
|
secret_value: secret_value,
|
||||||
default?: default?,
|
default?: default?,
|
||||||
secrets: secrets,
|
secrets: secrets,
|
||||||
agent_keys: agent_keys
|
agent_keys: agent_keys,
|
||||||
|
app_deployments: app_deployments
|
||||||
)
|
)
|
||||||
|> assign_new(:config_changeset, fn ->
|
|> assign_new(:config_changeset, fn ->
|
||||||
Hubs.Dockerfile.config_changeset(Hubs.Dockerfile.config_new())
|
Hubs.Dockerfile.config_changeset(Hubs.Dockerfile.config_new())
|
||||||
|
@ -188,6 +194,36 @@ defmodule LivebookWeb.Hub.Teams.DeploymentGroupLive do
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div :if={@deployment_group.mode == :online} class="flex flex-col space-y-4">
|
||||||
|
<h2 class="text-xl text-gray-800 font-medium pb-2 border-b border-gray-200">
|
||||||
|
Deployed Apps
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p class="text-gray-700">
|
||||||
|
Apps currently deployed in this group.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div id="deployed-apps-list" class="flex flex-col space-y-4">
|
||||||
|
<div class="flex flex-col space-y-4">
|
||||||
|
<.no_entries :if={@app_deployments == []}>
|
||||||
|
No deployed apps in this deployment group yet.
|
||||||
|
</.no_entries>
|
||||||
|
<div :if={@app_deployments != []}>
|
||||||
|
<.table id="hub-deployed-apps-table" rows={@app_deployments}>
|
||||||
|
<:col :let={app_deployment} label="ID"><%= app_deployment.id %></:col>
|
||||||
|
<:col :let={app_deployment} label="Slug"><%= app_deployment.slug %></:col>
|
||||||
|
<:col :let={app_deployment} label="Deployed By">
|
||||||
|
<%= app_deployment.deployed_by %>
|
||||||
|
</:col>
|
||||||
|
<:col :let={app_deployment} label="Deployed At">
|
||||||
|
<%= app_deployment.deployed_at %>
|
||||||
|
</:col>
|
||||||
|
</.table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col space-y-4">
|
<div class="flex flex-col space-y-4">
|
||||||
<h2 class="text-xl text-gray-800 font-medium pb-2 border-b border-gray-200">
|
<h2 class="text-xl text-gray-800 font-medium pb-2 border-b border-gray-200">
|
||||||
Airgapped deployment
|
Airgapped deployment
|
||||||
|
|
|
@ -17,6 +17,11 @@ defmodule LivebookWeb.SessionLive.AppDockerComponent do
|
||||||
socket = assign(socket, assigns)
|
socket = assign(socket, assigns)
|
||||||
deployment_groups = Provider.deployment_groups(assigns.hub)
|
deployment_groups = Provider.deployment_groups(assigns.hub)
|
||||||
|
|
||||||
|
deployment_group =
|
||||||
|
if assigns.deployment_group_id do
|
||||||
|
Enum.find(deployment_groups, &(&1.id == assigns.deployment_group_id))
|
||||||
|
end
|
||||||
|
|
||||||
socket =
|
socket =
|
||||||
socket
|
socket
|
||||||
|> assign(settings_valid?: Livebook.Notebook.AppSettings.valid?(socket.assigns.settings))
|
|> assign(settings_valid?: Livebook.Notebook.AppSettings.valid?(socket.assigns.settings))
|
||||||
|
@ -24,14 +29,18 @@ defmodule LivebookWeb.SessionLive.AppDockerComponent do
|
||||||
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_groups: deployment_groups,
|
||||||
|
deployment_group: deployment_group,
|
||||||
deployment_group_form: %{"deployment_group_id" => assigns.deployment_group_id},
|
deployment_group_form: %{"deployment_group_id" => assigns.deployment_group_id},
|
||||||
deployment_group_id: assigns.deployment_group_id
|
deployment_group_id: assigns.deployment_group_id
|
||||||
)
|
)
|
||||||
|> assign_new(:save_result, fn -> nil end)
|
|> assign_new(:messages, fn -> [] end)
|
||||||
|
|
||||||
socket =
|
socket =
|
||||||
if deployment_group_changed? do
|
if deployment_group_changed? do
|
||||||
assign(socket, :changeset, Hubs.Dockerfile.config_changeset(base_config(socket)))
|
assign(socket,
|
||||||
|
changeset: Hubs.Dockerfile.config_changeset(base_config(socket)),
|
||||||
|
deployment_type: :dockerfile
|
||||||
|
)
|
||||||
else
|
else
|
||||||
socket
|
socket
|
||||||
end
|
end
|
||||||
|
@ -40,9 +49,7 @@ defmodule LivebookWeb.SessionLive.AppDockerComponent do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp base_config(socket) do
|
defp base_config(socket) do
|
||||||
if id = socket.assigns.deployment_group_id do
|
if deployment_group = socket.assigns.deployment_group do
|
||||||
deployment_group = Enum.find(socket.assigns.deployment_groups, &(&1.id == id))
|
|
||||||
|
|
||||||
%{
|
%{
|
||||||
Hubs.Dockerfile.config_new()
|
Hubs.Dockerfile.config_new()
|
||||||
| clustering: deployment_group.clustering,
|
| clustering: deployment_group.clustering,
|
||||||
|
@ -65,14 +72,14 @@ defmodule LivebookWeb.SessionLive.AppDockerComponent do
|
||||||
file={@file}
|
file={@file}
|
||||||
settings_valid?={@settings_valid?}
|
settings_valid?={@settings_valid?}
|
||||||
hub={@hub}
|
hub={@hub}
|
||||||
|
deployment_group={@deployment_group}
|
||||||
deployment_groups={@deployment_groups}
|
deployment_groups={@deployment_groups}
|
||||||
deployment_group_form={@deployment_group_form}
|
deployment_group_form={@deployment_group_form}
|
||||||
deployment_group_id={@deployment_group_id}
|
deployment_group_id={@deployment_group_id}
|
||||||
changeset={@changeset}
|
changeset={@changeset}
|
||||||
session={@session}
|
session={@session}
|
||||||
dockerfile={@dockerfile}
|
dockerfile={@dockerfile}
|
||||||
warnings={@warnings}
|
messages={@messages}
|
||||||
save_result={@save_result}
|
|
||||||
myself={@myself}
|
myself={@myself}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -120,6 +127,7 @@ 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>
|
||||||
|
|
||||||
<div class="flex gap-12">
|
<div class="flex gap-12">
|
||||||
<p class="text-gray-700">
|
<p class="text-gray-700">
|
||||||
<.label>Hub</.label>
|
<.label>Hub</.label>
|
||||||
|
@ -155,11 +163,13 @@ defmodule LivebookWeb.SessionLive.AppDockerComponent do
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div :if={@warnings != []} class="flex flex-col gap-2">
|
|
||||||
<.message_box :for={warning <- @warnings} kind={:warning}>
|
<div :if={@messages != []} class="flex flex-col gap-2">
|
||||||
<%= raw(warning) %>
|
<.message_box :for={{kind, message} <- @messages} kind={kind}>
|
||||||
|
<%= raw(message) %>
|
||||||
</.message_box>
|
</.message_box>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<.form :let={f} for={@changeset} as={:data} phx-change="validate" phx-target={@myself}>
|
<.form :let={f} for={@changeset} as={:data} phx-change="validate" phx-target={@myself}>
|
||||||
<div class="flex flex-col space-y-4">
|
<div class="flex flex-col space-y-4">
|
||||||
<AppComponents.deployment_group_form_content
|
<AppComponents.deployment_group_form_content
|
||||||
|
@ -170,7 +180,7 @@ defmodule LivebookWeb.SessionLive.AppDockerComponent do
|
||||||
<AppComponents.docker_config_form_content hub={@hub} form={f} />
|
<AppComponents.docker_config_form_content hub={@hub} form={f} />
|
||||||
</div>
|
</div>
|
||||||
</.form>
|
</.form>
|
||||||
<.save_result :if={@save_result} save_result={@save_result} />
|
|
||||||
<AppComponents.docker_instructions
|
<AppComponents.docker_instructions
|
||||||
hub={@hub}
|
hub={@hub}
|
||||||
dockerfile={@dockerfile}
|
dockerfile={@dockerfile}
|
||||||
|
@ -194,22 +204,6 @@ defmodule LivebookWeb.SessionLive.AppDockerComponent do
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
defp save_result(%{save_result: {:ok, file}}) do
|
|
||||||
assigns = %{path: file.path}
|
|
||||||
|
|
||||||
~H"""
|
|
||||||
<.message_box kind={:info} message={"File saved at #{@path}"} />
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
defp save_result(%{save_result: {:error, message}}) do
|
|
||||||
assigns = %{message: message}
|
|
||||||
|
|
||||||
~H"""
|
|
||||||
<.message_box kind={:error} message={@message} />
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("validate", %{"data" => data}, socket) do
|
def handle_event("validate", %{"data" => data}, socket) do
|
||||||
changeset =
|
changeset =
|
||||||
|
@ -223,14 +217,12 @@ defmodule LivebookWeb.SessionLive.AppDockerComponent do
|
||||||
|
|
||||||
def handle_event("save_dockerfile", %{}, socket) do
|
def handle_event("save_dockerfile", %{}, socket) do
|
||||||
dockerfile_file = FileSystem.File.resolve(socket.assigns.file, "./Dockerfile")
|
dockerfile_file = FileSystem.File.resolve(socket.assigns.file, "./Dockerfile")
|
||||||
|
file_path_message = "File saved at #{dockerfile_file.path}"
|
||||||
|
|
||||||
save_result =
|
case FileSystem.File.write(dockerfile_file, socket.assigns.dockerfile) do
|
||||||
case FileSystem.File.write(dockerfile_file, socket.assigns.dockerfile) do
|
:ok -> {:noreply, assign(socket, messages: [{:info, file_path_message}])}
|
||||||
:ok -> {:ok, dockerfile_file}
|
{:error, message} -> {:noreply, assign(socket, messages: [{:error, message}])}
|
||||||
{:error, message} -> {:error, message}
|
end
|
||||||
end
|
|
||||||
|
|
||||||
{:noreply, assign(socket, save_result: save_result)}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("select_deployment_group", %{"deployment_group_id" => id}, socket) do
|
def handle_event("select_deployment_group", %{"deployment_group_id" => id}, socket) do
|
||||||
|
@ -241,7 +233,7 @@ defmodule LivebookWeb.SessionLive.AppDockerComponent do
|
||||||
end
|
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, messages: [])
|
||||||
end
|
end
|
||||||
|
|
||||||
defp update_dockerfile(socket) do
|
defp update_dockerfile(socket) do
|
||||||
|
@ -291,7 +283,9 @@ defmodule LivebookWeb.SessionLive.AppDockerComponent do
|
||||||
secrets
|
secrets
|
||||||
)
|
)
|
||||||
|
|
||||||
assign(socket, dockerfile: dockerfile, warnings: warnings)
|
messages = Enum.map(warnings, &{:warning, &1})
|
||||||
|
|
||||||
|
assign(socket, dockerfile: dockerfile, messages: messages)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp deployment_group_options(deployment_groups) do
|
defp deployment_group_options(deployment_groups) do
|
||||||
|
|
223
lib/livebook_web/live/session_live/app_teams_component.ex
Normal file
223
lib/livebook_web/live/session_live/app_teams_component.ex
Normal file
|
@ -0,0 +1,223 @@
|
||||||
|
defmodule LivebookWeb.SessionLive.AppTeamsComponent do
|
||||||
|
use LivebookWeb, :live_component
|
||||||
|
|
||||||
|
alias Livebook.Hubs.Provider
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def update(assigns, socket) do
|
||||||
|
socket = assign(socket, assigns)
|
||||||
|
deployment_groups = Provider.deployment_groups(assigns.hub)
|
||||||
|
|
||||||
|
deployment_group =
|
||||||
|
if assigns.deployment_group_id do
|
||||||
|
Enum.find(deployment_groups, &(&1.id == assigns.deployment_group_id))
|
||||||
|
end
|
||||||
|
|
||||||
|
app_deployment =
|
||||||
|
if deployment_group do
|
||||||
|
Enum.find(deployment_group.app_deployments, &(&1.slug == assigns.app_settings.slug))
|
||||||
|
end
|
||||||
|
|
||||||
|
socket =
|
||||||
|
socket
|
||||||
|
|> assign(
|
||||||
|
settings_valid?: Livebook.Notebook.AppSettings.valid?(socket.assigns.app_settings),
|
||||||
|
app_deployment: app_deployment,
|
||||||
|
deployment_groups: deployment_groups,
|
||||||
|
deployment_group: deployment_group,
|
||||||
|
deployment_group_form: %{"deployment_group_id" => assigns.deployment_group_id},
|
||||||
|
deployment_group_id: assigns.deployment_group_id
|
||||||
|
)
|
||||||
|
|> assign_new(:messages, fn -> [] end)
|
||||||
|
|
||||||
|
{:ok, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def render(assigns) do
|
||||||
|
~H"""
|
||||||
|
<div class="p-6 flex flex-col space-y-8">
|
||||||
|
<h3 class="text-2xl font-semibold text-gray-800">
|
||||||
|
App deployment with Livebook Teams
|
||||||
|
</h3>
|
||||||
|
<.content
|
||||||
|
file={@file}
|
||||||
|
hub={@hub}
|
||||||
|
app_deployment={@app_deployment}
|
||||||
|
deployment_group={@deployment_group}
|
||||||
|
deployment_groups={@deployment_groups}
|
||||||
|
deployment_group_form={@deployment_group_form}
|
||||||
|
deployment_group_id={@deployment_group_id}
|
||||||
|
session={@session}
|
||||||
|
messages={@messages}
|
||||||
|
myself={@myself}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
defp content(%{settings_valid?: false} = assigns) do
|
||||||
|
~H"""
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<p class="text-gray-700">
|
||||||
|
To deploy this app, make sure to specify valid settings.
|
||||||
|
</p>
|
||||||
|
<.link
|
||||||
|
class="text-blue-600 font-medium"
|
||||||
|
patch={~p"/sessions/#{@session.id}/settings/app?context=app-teams"}
|
||||||
|
>
|
||||||
|
<span>Configure</span>
|
||||||
|
<.remix_icon icon="arrow-right-line" />
|
||||||
|
</.link>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
defp content(assigns) do
|
||||||
|
~H"""
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<p class="text-gray-700">
|
||||||
|
You can deploy this app in the to your own cloud using Livebook Teams. To do that, select
|
||||||
|
which deployment group you want to send this app and then click the button to Deploy.
|
||||||
|
</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
|
||||||
|
for={@deployment_group_form}
|
||||||
|
phx-change="select_deployment_group"
|
||||||
|
phx-target={@myself}
|
||||||
|
id="select_deployment_group_form"
|
||||||
|
>
|
||||||
|
<.select_field
|
||||||
|
help={deployment_group_help()}
|
||||||
|
field={@deployment_group_form[:deployment_group_id]}
|
||||||
|
options={deployment_group_options(@deployment_groups)}
|
||||||
|
label="Deployment Group"
|
||||||
|
name="deployment_group_id"
|
||||||
|
value={@deployment_group_id}
|
||||||
|
/>
|
||||||
|
</.form>
|
||||||
|
<% else %>
|
||||||
|
<p class="text-gray-700">
|
||||||
|
<.label help={deployment_group_help()}>
|
||||||
|
Deployment Group
|
||||||
|
</.label>
|
||||||
|
<span>No deployment groups available</span>
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div :if={@messages != []} class="flex flex-col gap-2">
|
||||||
|
<.message_box :for={{kind, message} <- @messages} kind={kind}>
|
||||||
|
<%= raw(message) %>
|
||||||
|
</.message_box>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div :if={@app_deployment} class="space-y-3 pb-4">
|
||||||
|
<p class="text-gray-700">Current deployed version:</p>
|
||||||
|
|
||||||
|
<ul class="text-gray-700 space-y-3">
|
||||||
|
<li class="flex gap-2">
|
||||||
|
<div class="font-bold">Title:</div>
|
||||||
|
<span><%= @app_deployment.title %></span>
|
||||||
|
</li>
|
||||||
|
<li class="flex gap-2">
|
||||||
|
<div class="font-bold">Deployed by:</div>
|
||||||
|
<span><%= @app_deployment.deployed_by %></span>
|
||||||
|
</li>
|
||||||
|
<li class="flex gap-2">
|
||||||
|
<div class="font-bold">Deployed at:</div>
|
||||||
|
<span><%= @app_deployment.deployed_at %></span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<.button
|
||||||
|
id="deploy-livebook-agent-button"
|
||||||
|
color="blue"
|
||||||
|
phx-click="deploy_app"
|
||||||
|
phx-target={@myself}
|
||||||
|
>
|
||||||
|
<.remix_icon icon="rocket-line" /> Deploy to Livebook Agent
|
||||||
|
</.button>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event("select_deployment_group", %{"deployment_group_id" => id}, socket) do
|
||||||
|
id = if(id != "", do: id)
|
||||||
|
Livebook.Session.set_notebook_deployment_group(socket.assigns.session.pid, id)
|
||||||
|
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("deploy_app", _, socket) do
|
||||||
|
with {:ok, app_deployment} <- pack_app(socket),
|
||||||
|
:ok <- deploy_app(socket, app_deployment) do
|
||||||
|
message =
|
||||||
|
"App deployment for #{app_deployment.slug} with title #{app_deployment.title} created successfully"
|
||||||
|
|
||||||
|
{:noreply, assign(socket, messages: [{:info, message}])}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp pack_app(socket) do
|
||||||
|
notebook = Livebook.Session.get_notebook(socket.assigns.session.pid)
|
||||||
|
files_dir = socket.assigns.session.files_dir
|
||||||
|
|
||||||
|
case Livebook.Teams.AppDeployment.new(notebook, files_dir) do
|
||||||
|
{:ok, app_deployment} ->
|
||||||
|
{:ok, app_deployment}
|
||||||
|
|
||||||
|
{:warning, warnings} ->
|
||||||
|
messages = Enum.map(warnings, &{:error, &1})
|
||||||
|
{:noreply, assign(socket, messages: messages)}
|
||||||
|
|
||||||
|
{:error, error} ->
|
||||||
|
error = "Failed to pack files: #{error}"
|
||||||
|
{:noreply, assign(socket, messages: [{:error, error}])}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp deploy_app(socket, app_deployment) do
|
||||||
|
app_deployment = Map.replace!(app_deployment, :slug, "@bc")
|
||||||
|
|
||||||
|
case Livebook.Teams.deploy_app(socket.assigns.hub, app_deployment) do
|
||||||
|
:ok ->
|
||||||
|
:ok
|
||||||
|
|
||||||
|
{:error, %{errors: errors}} ->
|
||||||
|
errors = Enum.map(errors, fn {key, error} -> "#{key}: #{normalize_error(error)}" end)
|
||||||
|
{:noreply, assign(socket, messages: errors)}
|
||||||
|
|
||||||
|
{:transport_error, error} ->
|
||||||
|
{:noreply, assign(socket, messages: [{:error, error}])}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp normalize_error({msg, opts}) do
|
||||||
|
Enum.reduce(opts, msg, fn {key, value}, acc ->
|
||||||
|
String.replace(acc, "%{#{key}}", to_string(value))
|
||||||
|
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
|
||||||
|
|
||||||
|
defp deployment_group_help() do
|
||||||
|
"Share deployment credentials, secrets, and configuration with deployment groups."
|
||||||
|
end
|
||||||
|
end
|
|
@ -116,6 +116,24 @@ defmodule LivebookWeb.SessionLive.Render do
|
||||||
/>
|
/>
|
||||||
</.modal>
|
</.modal>
|
||||||
|
|
||||||
|
<.modal
|
||||||
|
:if={@live_action == :app_teams}
|
||||||
|
id="app-teams-modal"
|
||||||
|
show
|
||||||
|
width={:medium}
|
||||||
|
patch={@self_path}
|
||||||
|
>
|
||||||
|
<.live_component
|
||||||
|
module={LivebookWeb.SessionLive.AppTeamsComponent}
|
||||||
|
id="app-teams"
|
||||||
|
session={@session}
|
||||||
|
hub={@data_view.hub}
|
||||||
|
file={@data_view.file}
|
||||||
|
app_settings={@data_view.app_settings}
|
||||||
|
deployment_group_id={@data_view.deployment_group_id}
|
||||||
|
/>
|
||||||
|
</.modal>
|
||||||
|
|
||||||
<.modal
|
<.modal
|
||||||
:if={@live_action == :add_file_entry}
|
:if={@live_action == :add_file_entry}
|
||||||
id="add-file-entry-modal"
|
id="add-file-entry-modal"
|
||||||
|
|
|
@ -109,6 +109,7 @@ defmodule LivebookWeb.Router do
|
||||||
live "/sessions/:id/settings/file", SessionLive, :file_settings
|
live "/sessions/:id/settings/file", SessionLive, :file_settings
|
||||||
live "/sessions/:id/settings/app", SessionLive, :app_settings
|
live "/sessions/:id/settings/app", SessionLive, :app_settings
|
||||||
live "/sessions/:id/app-docker", SessionLive, :app_docker
|
live "/sessions/:id/app-docker", SessionLive, :app_docker
|
||||||
|
live "/sessions/:id/app-teams", SessionLive, :app_teams
|
||||||
live "/sessions/:id/add-file/:tab", SessionLive, :add_file_entry
|
live "/sessions/:id/add-file/:tab", SessionLive, :add_file_entry
|
||||||
live "/sessions/:id/rename-file/:name", SessionLive, :rename_file_entry
|
live "/sessions/:id/rename-file/:name", SessionLive, :rename_file_entry
|
||||||
live "/sessions/:id/bin", SessionLive, :bin
|
live "/sessions/:id/bin", SessionLive, :bin
|
||||||
|
|
|
@ -151,6 +151,43 @@ defmodule Livebook.Hubs.TeamClientTest do
|
||||||
# we dispatch the `{:event, :deployment_group_updated, :deployment_group}` event
|
# we dispatch the `{:event, :deployment_group_updated, :deployment_group}` event
|
||||||
assert_receive {:deployment_group_updated, %{id: ^id, agent_keys: [^built_in_agent_key]}}
|
assert_receive {:deployment_group_updated, %{id: ^id, agent_keys: [^built_in_agent_key]}}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@tag :tmp_dir
|
||||||
|
test "receives the app events", %{team: team, tmp_dir: tmp_dir} do
|
||||||
|
deployment_group = build(:deployment_group, name: team.id, mode: :online)
|
||||||
|
assert {:ok, id} = Livebook.Teams.create_deployment_group(team, deployment_group)
|
||||||
|
|
||||||
|
id = to_string(id)
|
||||||
|
|
||||||
|
# receives `{:event, :deployment_group_created, :deployment_group}` event
|
||||||
|
assert_receive {:deployment_group_created, %{id: ^id, app_deployments: []}}
|
||||||
|
|
||||||
|
# creates the app deployment
|
||||||
|
slug = Livebook.Utils.random_short_id()
|
||||||
|
title = "MyNotebook-#{slug}"
|
||||||
|
|
||||||
|
notebook = %{
|
||||||
|
Livebook.Notebook.new()
|
||||||
|
| app_settings: %{Livebook.Notebook.AppSettings.new() | slug: slug},
|
||||||
|
name: title,
|
||||||
|
hub_id: team.id,
|
||||||
|
deployment_group_id: id
|
||||||
|
}
|
||||||
|
|
||||||
|
files_dir = Livebook.FileSystem.File.local(tmp_dir)
|
||||||
|
{:ok, app_deployment} = Livebook.Teams.AppDeployment.new(notebook, files_dir)
|
||||||
|
:ok = Livebook.Teams.deploy_app(team, app_deployment)
|
||||||
|
|
||||||
|
# since the `app_deployment` belongs to a deployment group,
|
||||||
|
# we dispatch the `{:event, :deployment_group_updated, :deployment_group}` event
|
||||||
|
assert_receive {:deployment_group_updated,
|
||||||
|
%{
|
||||||
|
id: ^id,
|
||||||
|
app_deployments: [
|
||||||
|
%Livebook.Teams.AppDeployment{title: ^title, slug: ^slug}
|
||||||
|
]
|
||||||
|
}}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "handle user_connected event" do
|
describe "handle user_connected event" do
|
||||||
|
@ -273,8 +310,7 @@ defmodule Livebook.Hubs.TeamClientTest do
|
||||||
id: "1",
|
id: "1",
|
||||||
name: "sleepy-cat-#{System.unique_integer([:positive])}",
|
name: "sleepy-cat-#{System.unique_integer([:positive])}",
|
||||||
mode: :offline,
|
mode: :offline,
|
||||||
hub_id: team.id,
|
hub_id: team.id
|
||||||
secrets: []
|
|
||||||
)
|
)
|
||||||
|
|
||||||
livebook_proto_deployment_group =
|
livebook_proto_deployment_group =
|
||||||
|
@ -282,7 +318,9 @@ defmodule Livebook.Hubs.TeamClientTest do
|
||||||
id: to_string(deployment_group.id),
|
id: to_string(deployment_group.id),
|
||||||
name: deployment_group.name,
|
name: deployment_group.name,
|
||||||
mode: to_string(deployment_group.mode),
|
mode: to_string(deployment_group.mode),
|
||||||
secrets: []
|
secrets: [],
|
||||||
|
agent_keys: [],
|
||||||
|
deployed_apps: []
|
||||||
}
|
}
|
||||||
|
|
||||||
# creates the deployment group
|
# creates the deployment group
|
||||||
|
@ -317,6 +355,73 @@ defmodule Livebook.Hubs.TeamClientTest do
|
||||||
assert_receive {:deployment_group_deleted, ^updated_deployment_group}
|
assert_receive {:deployment_group_deleted, ^updated_deployment_group}
|
||||||
refute updated_deployment_group in TeamClient.get_deployment_groups(team.id)
|
refute updated_deployment_group in TeamClient.get_deployment_groups(team.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "dispatches the app deployments list", %{team: team, user_connected: user_connected} do
|
||||||
|
deployment_group =
|
||||||
|
build(:deployment_group,
|
||||||
|
id: "1",
|
||||||
|
name: "sleepy-cat-#{System.unique_integer([:positive])}",
|
||||||
|
mode: :online,
|
||||||
|
hub_id: team.id
|
||||||
|
)
|
||||||
|
|
||||||
|
livebook_proto_deployment_group =
|
||||||
|
%LivebookProto.DeploymentGroup{
|
||||||
|
id: to_string(deployment_group.id),
|
||||||
|
name: deployment_group.name,
|
||||||
|
mode: to_string(deployment_group.mode),
|
||||||
|
secrets: [],
|
||||||
|
agent_keys: [],
|
||||||
|
deployed_apps: []
|
||||||
|
}
|
||||||
|
|
||||||
|
user_connected = %{user_connected | deployment_groups: [livebook_proto_deployment_group]}
|
||||||
|
pid = connect_to_teams(team)
|
||||||
|
refute_receive {:deployment_group_created, ^deployment_group}
|
||||||
|
send(pid, {:event, :user_connected, user_connected})
|
||||||
|
assert_receive {:deployment_group_created, ^deployment_group}
|
||||||
|
assert deployment_group in TeamClient.get_deployment_groups(team.id)
|
||||||
|
|
||||||
|
url = "http://localhost/123456.zip"
|
||||||
|
|
||||||
|
app_deployment =
|
||||||
|
build(:app_deployment,
|
||||||
|
filename: "123456.zip",
|
||||||
|
file: {:url, url},
|
||||||
|
deployment_group_id: deployment_group.id
|
||||||
|
)
|
||||||
|
|
||||||
|
# updates the deployment group with an app deployment
|
||||||
|
updated_deployment_group = %{deployment_group | app_deployments: [app_deployment]}
|
||||||
|
|
||||||
|
livebook_proto_app_deployment =
|
||||||
|
%LivebookProto.DeployedApp{
|
||||||
|
id: app_deployment.id,
|
||||||
|
title: app_deployment.title,
|
||||||
|
slug: app_deployment.slug,
|
||||||
|
sha: app_deployment.sha,
|
||||||
|
archive_url: url,
|
||||||
|
deployed_by: app_deployment.deployed_by,
|
||||||
|
deployed_at: to_string(app_deployment.deployed_at),
|
||||||
|
app_id: "1",
|
||||||
|
deployment_group_id: app_deployment.deployment_group_id
|
||||||
|
}
|
||||||
|
|
||||||
|
updated_livebook_proto_deployment_group = %{
|
||||||
|
livebook_proto_deployment_group
|
||||||
|
| deployed_apps: [livebook_proto_app_deployment]
|
||||||
|
}
|
||||||
|
|
||||||
|
user_connected = %{
|
||||||
|
user_connected
|
||||||
|
| deployment_groups: [updated_livebook_proto_deployment_group]
|
||||||
|
}
|
||||||
|
|
||||||
|
send(pid, {:event, :user_connected, user_connected})
|
||||||
|
assert_receive {:deployment_group_updated, ^updated_deployment_group}
|
||||||
|
refute deployment_group in TeamClient.get_deployment_groups(team.id)
|
||||||
|
assert updated_deployment_group in TeamClient.get_deployment_groups(team.id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "handle agent_connected event" do
|
describe "handle agent_connected event" do
|
||||||
|
@ -335,7 +440,11 @@ defmodule Livebook.Hubs.TeamClientTest do
|
||||||
deployment_groups: []
|
deployment_groups: []
|
||||||
}
|
}
|
||||||
|
|
||||||
{:ok, team: team, deployment_group: deployment_group, agent_connected: agent_connected}
|
{:ok,
|
||||||
|
team: team,
|
||||||
|
deployment_group: deployment_group,
|
||||||
|
agent_connected: agent_connected,
|
||||||
|
agent_key: agent_key}
|
||||||
end
|
end
|
||||||
|
|
||||||
test "dispatches the secrets list", %{team: team, agent_connected: agent_connected} do
|
test "dispatches the secrets list", %{team: team, agent_connected: agent_connected} do
|
||||||
|
@ -548,6 +657,151 @@ defmodule Livebook.Hubs.TeamClientTest do
|
||||||
refute secret in TeamClient.get_secrets(team.id)
|
refute secret in TeamClient.get_secrets(team.id)
|
||||||
assert override_secret in TeamClient.get_secrets(team.id)
|
assert override_secret in TeamClient.get_secrets(team.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@tag :tmp_dir
|
||||||
|
test "dispatches the app deployments list",
|
||||||
|
%{
|
||||||
|
team: team,
|
||||||
|
deployment_group: teams_deployment_group,
|
||||||
|
agent_key: teams_agent_key,
|
||||||
|
agent_connected: agent_connected,
|
||||||
|
tmp_dir: tmp_dir
|
||||||
|
} do
|
||||||
|
agent_key =
|
||||||
|
build(:agent_key,
|
||||||
|
id: to_string(teams_agent_key.id),
|
||||||
|
key: teams_agent_key.key,
|
||||||
|
deployment_group_id: to_string(teams_agent_key.deployment_group_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
deployment_group =
|
||||||
|
build(:deployment_group,
|
||||||
|
id: to_string(teams_deployment_group.id),
|
||||||
|
name: teams_deployment_group.name,
|
||||||
|
mode: teams_deployment_group.mode,
|
||||||
|
hub_id: team.id,
|
||||||
|
agent_keys: [agent_key]
|
||||||
|
)
|
||||||
|
|
||||||
|
livebook_proto_agent_key =
|
||||||
|
%LivebookProto.AgentKey{
|
||||||
|
id: agent_key.id,
|
||||||
|
key: agent_key.key,
|
||||||
|
deployment_group_id: agent_key.deployment_group_id
|
||||||
|
}
|
||||||
|
|
||||||
|
livebook_proto_deployment_group =
|
||||||
|
%LivebookProto.DeploymentGroup{
|
||||||
|
id: to_string(deployment_group.id),
|
||||||
|
name: deployment_group.name,
|
||||||
|
mode: to_string(deployment_group.mode),
|
||||||
|
secrets: [],
|
||||||
|
agent_keys: [livebook_proto_agent_key],
|
||||||
|
deployed_apps: []
|
||||||
|
}
|
||||||
|
|
||||||
|
agent_connected = %{agent_connected | deployment_groups: [livebook_proto_deployment_group]}
|
||||||
|
pid = connect_to_teams(team)
|
||||||
|
|
||||||
|
# Since we're connecting as Agent, we should receive the
|
||||||
|
# `:deployment_group_created` event from `:agent_connected` event
|
||||||
|
assert_receive {:deployment_group_created, ^deployment_group}
|
||||||
|
assert deployment_group in TeamClient.get_deployment_groups(team.id)
|
||||||
|
|
||||||
|
# creates a new app deployment
|
||||||
|
deployment_group_id = to_string(deployment_group.id)
|
||||||
|
slug = Livebook.Utils.random_short_id()
|
||||||
|
title = "MyNotebook2-#{slug}"
|
||||||
|
|
||||||
|
notebook = %{
|
||||||
|
Livebook.Notebook.new()
|
||||||
|
| app_settings: %{Livebook.Notebook.AppSettings.new() | slug: slug},
|
||||||
|
name: title,
|
||||||
|
hub_id: team.id,
|
||||||
|
deployment_group_id: deployment_group_id
|
||||||
|
}
|
||||||
|
|
||||||
|
files_dir = Livebook.FileSystem.File.local(tmp_dir)
|
||||||
|
|
||||||
|
{:ok, %{file: {:content, zip_content}} = app_deployment} =
|
||||||
|
Livebook.Teams.AppDeployment.new(notebook, files_dir)
|
||||||
|
|
||||||
|
secret_key = Livebook.Teams.derive_key(team.teams_key)
|
||||||
|
encrypted_content = Livebook.Teams.encrypt(zip_content, secret_key)
|
||||||
|
|
||||||
|
# Since the app deployment struct generation is from Livebook side,
|
||||||
|
# we don't have yet the information about who deployed the app,
|
||||||
|
# so we need to add it ourselves
|
||||||
|
app_deployment = %{
|
||||||
|
app_deployment
|
||||||
|
| id: "1",
|
||||||
|
filename: app_deployment.filename <> ".encrypted",
|
||||||
|
deployed_by: "Jake Peralta",
|
||||||
|
deployed_at: NaiveDateTime.utc_now()
|
||||||
|
}
|
||||||
|
|
||||||
|
bypass = Bypass.open()
|
||||||
|
|
||||||
|
Bypass.expect_once(bypass, "GET", "/#{app_deployment.filename}", fn conn ->
|
||||||
|
conn
|
||||||
|
|> Plug.Conn.put_resp_content_type("application/octet-stream")
|
||||||
|
|> Plug.Conn.resp(200, encrypted_content)
|
||||||
|
end)
|
||||||
|
|
||||||
|
# updates the deployment group with an app deployment
|
||||||
|
# and after we deploy, the `:file` key turns to `nil` value
|
||||||
|
updated_deployment_group = %{
|
||||||
|
deployment_group
|
||||||
|
| app_deployments: [%{app_deployment | file: nil}]
|
||||||
|
}
|
||||||
|
|
||||||
|
livebook_proto_app_deployment =
|
||||||
|
%LivebookProto.DeployedApp{
|
||||||
|
id: app_deployment.id,
|
||||||
|
title: app_deployment.title,
|
||||||
|
slug: app_deployment.slug,
|
||||||
|
sha: app_deployment.sha,
|
||||||
|
archive_url: "http://localhost:#{bypass.port}/#{app_deployment.filename}",
|
||||||
|
deployed_by: app_deployment.deployed_by,
|
||||||
|
deployed_at: to_string(app_deployment.deployed_at),
|
||||||
|
app_id: "1",
|
||||||
|
deployment_group_id: app_deployment.deployment_group_id
|
||||||
|
}
|
||||||
|
|
||||||
|
updated_livebook_proto_deployment_group = %{
|
||||||
|
livebook_proto_deployment_group
|
||||||
|
| deployed_apps: [livebook_proto_app_deployment]
|
||||||
|
}
|
||||||
|
|
||||||
|
agent_connected = %{
|
||||||
|
agent_connected
|
||||||
|
| deployment_groups: [updated_livebook_proto_deployment_group]
|
||||||
|
}
|
||||||
|
|
||||||
|
apps_path = Path.join(tmp_dir, "apps")
|
||||||
|
app_path = Path.join(apps_path, slug)
|
||||||
|
Application.put_env(:livebook, :apps_path, apps_path)
|
||||||
|
|
||||||
|
# To avoid having extracting to the same folder from the original notebook,
|
||||||
|
# we need to create the ./apps/{slug} folder before sending the event
|
||||||
|
File.mkdir_p!(app_path)
|
||||||
|
|
||||||
|
Livebook.Apps.subscribe()
|
||||||
|
|
||||||
|
send(pid, {:event, :agent_connected, agent_connected})
|
||||||
|
assert_receive {:deployment_group_updated, ^updated_deployment_group}
|
||||||
|
|
||||||
|
assert_receive {:app_created, %{pid: app_pid, slug: ^slug}}
|
||||||
|
|
||||||
|
assert_receive {:app_updated,
|
||||||
|
%{slug: ^slug, sessions: [%{app_status: %{execution: :executed}}]}}
|
||||||
|
|
||||||
|
refute deployment_group in TeamClient.get_deployment_groups(team.id)
|
||||||
|
assert updated_deployment_group in TeamClient.get_deployment_groups(team.id)
|
||||||
|
|
||||||
|
Livebook.App.close(app_pid)
|
||||||
|
Application.put_env(:livebook, :apps_path, nil)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp connect_to_teams(%{id: id} = team) do
|
defp connect_to_teams(%{id: id} = team) do
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
defmodule Livebook.TeamsTest do
|
defmodule Livebook.TeamsTest do
|
||||||
use Livebook.TeamsIntegrationCase, async: true
|
use Livebook.TeamsIntegrationCase, async: true
|
||||||
|
|
||||||
alias Livebook.Teams
|
alias Livebook.{Notebook, Teams, Utils}
|
||||||
alias Livebook.Teams.Org
|
alias Livebook.Teams.Org
|
||||||
|
|
||||||
describe "create_org/1" do
|
describe "create_org/1" do
|
||||||
|
@ -221,4 +221,27 @@ defmodule Livebook.TeamsTest do
|
||||||
assert "can't be blank" in errors_on(changeset).name
|
assert "can't be blank" in errors_on(changeset).name
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "deploy_app/2" do
|
||||||
|
@tag :tmp_dir
|
||||||
|
test "deploys app to Teams from a notebook", %{user: user, node: node, tmp_dir: tmp_dir} do
|
||||||
|
team = create_team_hub(user, node)
|
||||||
|
deployment_group = build(:deployment_group, name: "BAZ", mode: :online)
|
||||||
|
|
||||||
|
{:ok, id} = Teams.create_deployment_group(team, deployment_group)
|
||||||
|
|
||||||
|
notebook = %{
|
||||||
|
Notebook.new()
|
||||||
|
| app_settings: %{Notebook.AppSettings.new() | slug: Utils.random_short_id()},
|
||||||
|
name: "MyNotebook",
|
||||||
|
hub_id: team.id,
|
||||||
|
deployment_group_id: to_string(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
files_dir = Livebook.FileSystem.File.local(tmp_dir)
|
||||||
|
|
||||||
|
assert {:ok, app_deployment} = Teams.AppDeployment.new(notebook, files_dir)
|
||||||
|
assert Teams.deploy_app(team, app_deployment) == :ok
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,7 +4,7 @@ defmodule LivebookWeb.Integration.SessionLiveTest do
|
||||||
import Phoenix.LiveViewTest
|
import Phoenix.LiveViewTest
|
||||||
import Livebook.SessionHelpers
|
import Livebook.SessionHelpers
|
||||||
|
|
||||||
alias Livebook.{Sessions, Session}
|
alias Livebook.{FileSystem, Sessions, Session}
|
||||||
|
|
||||||
setup do
|
setup do
|
||||||
{:ok, session} = Sessions.create_session(notebook: Livebook.Notebook.new())
|
{:ok, session} = Sessions.create_session(notebook: Livebook.Notebook.new())
|
||||||
|
@ -291,7 +291,7 @@ defmodule LivebookWeb.Integration.SessionLiveTest do
|
||||||
|
|
||||||
team_file_system =
|
team_file_system =
|
||||||
build(:fs_s3,
|
build(:fs_s3,
|
||||||
id: Livebook.FileSystem.S3.id(team_id, bucket_url),
|
id: FileSystem.S3.id(team_id, bucket_url),
|
||||||
bucket_url: bucket_url,
|
bucket_url: bucket_url,
|
||||||
hub_id: team_id
|
hub_id: team_id
|
||||||
)
|
)
|
||||||
|
@ -334,7 +334,7 @@ defmodule LivebookWeb.Integration.SessionLiveTest do
|
||||||
|
|
||||||
file_system =
|
file_system =
|
||||||
build(:fs_s3,
|
build(:fs_s3,
|
||||||
id: Livebook.FileSystem.S3.id(hub_id, bucket_url),
|
id: FileSystem.S3.id(hub_id, bucket_url),
|
||||||
bucket_url: bucket_url,
|
bucket_url: bucket_url,
|
||||||
hub_id: hub_id,
|
hub_id: hub_id,
|
||||||
external_id: "123"
|
external_id: "123"
|
||||||
|
@ -378,7 +378,7 @@ defmodule LivebookWeb.Integration.SessionLiveTest do
|
||||||
assert_receive {:operation, {:set_notebook_hub, _client, ^team_id}}
|
assert_receive {:operation, {:set_notebook_hub, _client, ^team_id}}
|
||||||
|
|
||||||
notebook_path = Path.join(tmp_dir, "notebook.livemd")
|
notebook_path = Path.join(tmp_dir, "notebook.livemd")
|
||||||
file = Livebook.FileSystem.File.local(notebook_path)
|
file = FileSystem.File.local(notebook_path)
|
||||||
Session.set_file(session.pid, file)
|
Session.set_file(session.pid, file)
|
||||||
|
|
||||||
slug = Livebook.Utils.random_short_id()
|
slug = Livebook.Utils.random_short_id()
|
||||||
|
@ -415,7 +415,7 @@ defmodule LivebookWeb.Integration.SessionLiveTest do
|
||||||
assert_receive {:operation, {:set_notebook_hub, _client, ^team_id}}
|
assert_receive {:operation, {:set_notebook_hub, _client, ^team_id}}
|
||||||
|
|
||||||
notebook_path = Path.join(tmp_dir, "notebook.livemd")
|
notebook_path = Path.join(tmp_dir, "notebook.livemd")
|
||||||
file = Livebook.FileSystem.File.local(notebook_path)
|
file = FileSystem.File.local(notebook_path)
|
||||||
Session.set_file(session.pid, file)
|
Session.set_file(session.pid, file)
|
||||||
|
|
||||||
slug = Livebook.Utils.random_short_id()
|
slug = Livebook.Utils.random_short_id()
|
||||||
|
@ -447,7 +447,7 @@ defmodule LivebookWeb.Integration.SessionLiveTest do
|
||||||
assert_receive {:operation, {:set_notebook_hub, _client, ^team_id}}
|
assert_receive {:operation, {:set_notebook_hub, _client, ^team_id}}
|
||||||
|
|
||||||
notebook_path = Path.join(tmp_dir, "notebook.livemd")
|
notebook_path = Path.join(tmp_dir, "notebook.livemd")
|
||||||
file = Livebook.FileSystem.File.local(notebook_path)
|
file = FileSystem.File.local(notebook_path)
|
||||||
Session.set_file(session.pid, file)
|
Session.set_file(session.pid, file)
|
||||||
|
|
||||||
slug = Livebook.Utils.random_short_id()
|
slug = Livebook.Utils.random_short_id()
|
||||||
|
@ -460,5 +460,87 @@ defmodule LivebookWeb.Integration.SessionLiveTest do
|
||||||
assert render(view) =~ "No deployment groups available"
|
assert render(view) =~ "No deployment groups available"
|
||||||
refute has_element?(view, "#select_deployment_group_form")
|
refute has_element?(view, "#select_deployment_group_form")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@tag :tmp_dir
|
||||||
|
test "deploys the app to livebook teams api",
|
||||||
|
%{conn: conn, user: user, node: node, session: session, tmp_dir: tmp_dir} do
|
||||||
|
team = create_team_hub(user, node)
|
||||||
|
Session.set_notebook_hub(session.pid, team.id)
|
||||||
|
|
||||||
|
notebook_path = Path.join(tmp_dir, "notebook.livemd")
|
||||||
|
file = 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)
|
||||||
|
|
||||||
|
deployment_group = insert_deployment_group(mode: :online, hub_id: team.id)
|
||||||
|
Session.set_notebook_deployment_group(session.pid, deployment_group.id)
|
||||||
|
|
||||||
|
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}/app-teams")
|
||||||
|
|
||||||
|
assert render(view) =~ "Deploy to Livebook Agent"
|
||||||
|
|
||||||
|
view
|
||||||
|
|> element("#deploy-livebook-agent-button")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
assert render(view) =~
|
||||||
|
"App deployment for #{slug} with title Untitled notebook created successfully"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "deploys the app to livebook teams api without saving the file",
|
||||||
|
%{conn: conn, user: user, node: node, session: session} do
|
||||||
|
team = create_team_hub(user, node)
|
||||||
|
Session.set_notebook_hub(session.pid, team.id)
|
||||||
|
|
||||||
|
slug = Livebook.Utils.random_short_id()
|
||||||
|
app_settings = %{Livebook.Notebook.AppSettings.new() | slug: slug}
|
||||||
|
Session.set_app_settings(session.pid, app_settings)
|
||||||
|
|
||||||
|
deployment_group = insert_deployment_group(mode: :online, hub_id: team.id)
|
||||||
|
Session.set_notebook_deployment_group(session.pid, deployment_group.id)
|
||||||
|
|
||||||
|
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}/app-teams")
|
||||||
|
|
||||||
|
assert render(view) =~ "Deploy to Livebook Agent"
|
||||||
|
|
||||||
|
view
|
||||||
|
|> element("#deploy-livebook-agent-button")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
assert render(view) =~
|
||||||
|
"App deployment for #{slug} with title Untitled notebook created successfully"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns error when the deployment size is higher than the maximum size of 20MB",
|
||||||
|
%{conn: conn, user: user, node: node, session: session} do
|
||||||
|
team = create_team_hub(user, node)
|
||||||
|
Session.set_notebook_hub(session.pid, team.id)
|
||||||
|
|
||||||
|
slug = Livebook.Utils.random_short_id()
|
||||||
|
app_settings = %{Livebook.Notebook.AppSettings.new() | slug: slug}
|
||||||
|
Session.set_app_settings(session.pid, app_settings)
|
||||||
|
|
||||||
|
deployment_group = insert_deployment_group(mode: :online, hub_id: team.id)
|
||||||
|
Session.set_notebook_deployment_group(session.pid, deployment_group.id)
|
||||||
|
|
||||||
|
%{files_dir: files_dir} = session
|
||||||
|
image_file = FileSystem.File.resolve(files_dir, "image.jpg")
|
||||||
|
:ok = FileSystem.File.write(image_file, :crypto.strong_rand_bytes(20 * 1024 * 1024))
|
||||||
|
Session.add_file_entries(session.pid, [%{type: :attachment, name: "image.jpg"}])
|
||||||
|
|
||||||
|
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}/app-teams")
|
||||||
|
|
||||||
|
assert render(view) =~ "Deploy to Livebook Agent"
|
||||||
|
|
||||||
|
view
|
||||||
|
|> element("#deploy-livebook-agent-button")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
assert render(view) =~
|
||||||
|
"Failed to pack files: the notebook and its attachments have exceeded the maximum size of 20MB"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -61,7 +61,8 @@ defmodule Livebook.Factory do
|
||||||
name: "FOO",
|
name: "FOO",
|
||||||
mode: :offline,
|
mode: :offline,
|
||||||
agent_keys: [],
|
agent_keys: [],
|
||||||
secrets: []
|
secrets: [],
|
||||||
|
app_deployments: []
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -91,6 +92,37 @@ defmodule Livebook.Factory do
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def build(:agent_key) do
|
||||||
|
%Livebook.Teams.AgentKey{
|
||||||
|
id: "1",
|
||||||
|
key: "lb_ak_zj9tWM1rEVeweYR7DbH_2VK5_aKtWfptcL07dBncqg",
|
||||||
|
deployment_group_id: "1"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def build(:app_deployment) do
|
||||||
|
slug = Livebook.Utils.random_short_id()
|
||||||
|
path = Plug.Upload.random_file!(slug)
|
||||||
|
local = Livebook.FileSystem.Local.new()
|
||||||
|
file = Livebook.FileSystem.File.new(local, path)
|
||||||
|
{:ok, content} = Livebook.FileSystem.File.read(file)
|
||||||
|
|
||||||
|
md5_hash = :crypto.hash(:md5, content)
|
||||||
|
shasum = Base.encode16(md5_hash, case: :lower)
|
||||||
|
|
||||||
|
%Livebook.Teams.AppDeployment{
|
||||||
|
id: "1",
|
||||||
|
title: "MyNotebook",
|
||||||
|
sha: shasum,
|
||||||
|
slug: slug,
|
||||||
|
file: file,
|
||||||
|
filename: Path.basename(file.path),
|
||||||
|
deployment_group_id: "1",
|
||||||
|
deployed_by: "Ada Lovelace",
|
||||||
|
deployed_at: NaiveDateTime.utc_now()
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
def build(factory_name, attrs) do
|
def build(factory_name, attrs) do
|
||||||
factory_name |> build() |> struct!(attrs)
|
factory_name |> build() |> struct!(attrs)
|
||||||
end
|
end
|
||||||
|
|
Loading…
Add table
Reference in a new issue