Add Enterprise Hub (#1449)

This commit is contained in:
Alexandre de Souza 2022-10-11 11:27:27 -03:00 committed by GitHub
parent 5c053a9573
commit 19773a0e36
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 1021 additions and 28 deletions

View file

@ -2,7 +2,7 @@ defmodule Livebook.Hubs do
@moduledoc false
alias Livebook.Storage
alias Livebook.Hubs.{Fly, Local, Metadata, Provider}
alias Livebook.Hubs.{Enterprise, Fly, Local, Metadata, Provider}
@namespace :hubs
@ -105,6 +105,10 @@ defmodule Livebook.Hubs do
Provider.load(%Fly{}, fields)
end
defp to_struct(%{id: "enterprise-" <> _} = fields) do
Provider.load(%Enterprise{}, fields)
end
defp to_struct(%{id: "local-" <> _} = fields) do
Provider.load(%Local{}, fields)
end

View file

@ -0,0 +1,130 @@
defmodule Livebook.Hubs.Enterprise do
@moduledoc false
use Ecto.Schema
import Ecto.Changeset
alias Livebook.Hubs
@type t :: %__MODULE__{
id: Livebook.Utils.id(),
url: String.t(),
token: String.t(),
external_id: String.t(),
hub_name: String.t(),
hub_color: String.t()
}
embedded_schema do
field :url, :string
field :token, :string
field :external_id, :string
field :hub_name, :string
field :hub_color, Livebook.EctoTypes.HexColor
end
@fields ~w(
url
token
external_id
hub_name
hub_color
)a
@doc """
Returns an `%Ecto.Changeset{}` for tracking hub changes.
"""
@spec change_hub(t(), map()) :: Ecto.Changeset.t()
def change_hub(%__MODULE__{} = enterprise, attrs \\ %{}) do
enterprise
|> changeset(attrs)
|> Map.put(:action, :validate)
end
@doc """
Creates a Hub.
With success, notifies interested processes about hub metadatas data change.
Otherwise, it will return an error tuple with changeset.
"""
@spec create_hub(t(), map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()}
def create_hub(%__MODULE__{} = enterprise, attrs) do
changeset = changeset(enterprise, attrs)
id = get_field(changeset, :id)
if Hubs.hub_exists?(id) do
{:error,
changeset
|> add_error(:external_id, "already exists")
|> Map.replace!(:action, :validate)}
else
with {:ok, struct} <- apply_action(changeset, :insert) do
Hubs.save_hub(struct)
{:ok, struct}
end
end
end
@doc """
Updates a Hub.
With success, notifies interested processes about hub metadatas data change.
Otherwise, it will return an error tuple with changeset.
"""
@spec update_hub(t(), map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()}
def update_hub(%__MODULE__{} = enterprise, attrs) do
changeset = changeset(enterprise, attrs)
id = get_field(changeset, :id)
if Hubs.hub_exists?(id) do
with {:ok, struct} <- apply_action(changeset, :update) do
Hubs.save_hub(struct)
{:ok, struct}
end
else
{:error,
changeset
|> add_error(:external_id, "does not exists")
|> Map.replace!(:action, :validate)}
end
end
defp changeset(enterprise, attrs) do
enterprise
|> cast(attrs, @fields)
|> validate_required(@fields)
|> add_id()
end
defp add_id(changeset) do
case get_field(changeset, :external_id) do
nil -> changeset
external_id -> put_change(changeset, :id, "enterprise-#{external_id}")
end
end
end
defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Enterprise do
def load(%Livebook.Hubs.Enterprise{} = enterprise, fields) do
%{
enterprise
| id: fields.id,
url: fields.url,
token: fields.token,
external_id: fields.external_id,
hub_name: fields.hub_name,
hub_color: fields.hub_color
}
end
def normalize(%Livebook.Hubs.Enterprise{} = enterprise) do
%Livebook.Hubs.Metadata{
id: enterprise.id,
name: enterprise.hub_name,
provider: enterprise,
color: enterprise.hub_color
}
end
def type(_), do: "enterprise"
end

View file

@ -0,0 +1,62 @@
defmodule Livebook.Hubs.EnterpriseClient do
@moduledoc false
alias Livebook.Utils.HTTP
@path "/api/v1"
def fetch_info(url, token) do
query = """
query {
info {
id
}
}
"""
with {:ok, %{"info" => info}} <- graphql(url, token, query) do
{:ok, info}
end
end
def fetch_me(url, token) do
query = """
query {
me {
id
}
}
"""
with {:ok, %{"me" => me}} <- graphql(url, token, query) do
{:ok, me}
end
end
defp graphql(url, token, query, input \\ %{}) do
headers = [{"Authorization", "Bearer #{token}"}]
body = {"application/json", Jason.encode!(%{query: query, variables: input})}
case HTTP.request(:post, graphql_endpoint(url), headers: headers, body: body) do
{:ok, 200, _, body} ->
case Jason.decode!(body) do
%{"errors" => [%{"message" => "invalid_token"}]} ->
{:error, "request failed with invalid token", :invalid_token}
%{"errors" => [%{"message" => "unauthorized"}]} ->
{:error, "request failed with unauthorized", :unauthorized}
%{"errors" => [%{"message" => message}]} ->
{:error, "request failed with message: #{message}", :other}
%{"data" => data} ->
{:ok, data}
end
{:error, {:failed_connect, _}} ->
{:error, "request failed to connect", :invalid_url}
end
end
defp graphql_endpoint(url), do: url <> @path
end

View file

@ -0,0 +1,81 @@
defmodule LivebookWeb.Hub.Edit.EnterpriseComponent do
use LivebookWeb, :live_component
alias Livebook.EctoTypes.HexColor
alias Livebook.Hubs.Enterprise
@impl true
def update(assigns, socket) do
changeset = Enterprise.change_hub(assigns.hub)
{:ok,
socket
|> assign(assigns)
|> assign(changeset: changeset)}
end
@impl true
def render(assigns) do
~H"""
<div id={"#{@id}-component"}>
<div class="flex flex-col space-y-10">
<div class="flex flex-col space-y-2">
<h2 class="text-xl text-gray-800 font-medium pb-2 border-b border-gray-200">
General
</h2>
<.form
:let={f}
id={@id}
class="flex flex-col mt-4 space-y-4"
for={@changeset}
phx-submit="save"
phx-change="validate"
phx-target={@myself}
phx-debounce="blur"
>
<div class="grid grid-cols-1 md:grid-cols-1 gap-3">
<.input_wrapper form={f} field={:hub_color} class="flex flex-col space-y-1">
<div class="input-label">Color</div>
<.hex_color_input
form={f}
field={:hub_color}
randomize={JS.push("randomize_color", target: @myself)}
/>
</.input_wrapper>
</div>
<%= submit("Update Hub",
class: "button-base button-blue",
phx_disable_with: "Updating...",
disabled: not @changeset.valid?
) %>
</.form>
</div>
</div>
</div>
"""
end
@impl true
def handle_event("randomize_color", _, socket) do
handle_event("validate", %{"enterprise" => %{"hub_color" => HexColor.random()}}, socket)
end
def handle_event("save", %{"enterprise" => params}, socket) do
case Enterprise.update_hub(socket.assigns.hub, params) do
{:ok, hub} ->
{:noreply,
socket
|> put_flash(:success, "Hub updated successfully")
|> push_redirect(to: Routes.hub_path(socket, :edit, hub.id))}
{:error, changeset} ->
{:noreply, assign(socket, changeset: changeset)}
end
end
def handle_event("validate", %{"enterprise" => attrs}, socket) do
{:noreply, assign(socket, changeset: Enterprise.change_hub(socket.assigns.hub, attrs))}
end
end

View file

@ -50,6 +50,14 @@ defmodule LivebookWeb.Hub.EditLive do
env_var_id={@env_var_id}
/>
<% end %>
<%= if @type == "enterprise" do %>
<.live_component
module={LivebookWeb.Hub.Edit.EnterpriseComponent}
hub={@hub}
id="enterprise-form"
/>
<% end %>
</div>
</LayoutHelpers.layout>
"""

View file

@ -0,0 +1,157 @@
defmodule LivebookWeb.Hub.New.EnterpriseComponent do
use LivebookWeb, :live_component
import Ecto.Changeset, only: [get_field: 2]
alias Livebook.EctoTypes.HexColor
alias Livebook.Hubs.{Enterprise, EnterpriseClient}
@impl true
def update(assigns, socket) do
{:ok,
socket
|> assign(assigns)
|> assign(
base: %Enterprise{},
changeset: Enterprise.change_hub(%Enterprise{}),
connected: false
)}
end
@impl true
def render(assigns) do
~H"""
<div>
<.form
:let={f}
id={@id}
class="flex flex-col space-y-4"
for={@changeset}
phx-submit="save"
phx-change="validate"
phx-target={@myself}
phx-debounce="blur"
>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<.input_wrapper form={f} field={:url} class="flex flex-col space-y-1">
<div class="input-label">URL</div>
<%= text_input(f, :url,
class: "input w-full phx-form-error:border-red-300",
autofocus: true,
spellcheck: "false",
autocomplete: "off"
) %>
</.input_wrapper>
<.input_wrapper form={f} field={:token} class="flex flex-col space-y-1">
<div class="input-label">Token</div>
<%= password_input(f, :token,
value: get_field(@changeset, :token),
class: "input w-full phx-form-error:border-red-300",
spellcheck: "false",
autocomplete: "off"
) %>
</.input_wrapper>
</div>
<button
id="connect"
type="button"
phx-click="connect"
phx-target={@myself}
class="button-base button-blue"
>
Connect
</button>
<%= if @connected do %>
<div class="grid grid-cols-1 md:grid-cols-1">
<.input_wrapper form={f} field={:external_id} class="flex flex-col space-y-1">
<div class="input-label">ID</div>
<%= text_input(f, :external_id, class: "input", disabled: true) %>
</.input_wrapper>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<.input_wrapper form={f} field={:hub_name} class="flex flex-col space-y-1">
<div class="input-label">Name</div>
<%= text_input(f, :hub_name, class: "input", readonly: true) %>
</.input_wrapper>
<.input_wrapper form={f} field={:hub_color} class="flex flex-col space-y-1">
<div class="input-label">Color</div>
<.hex_color_input
form={f}
field={:hub_color}
randomize={JS.push("randomize_color", target: @myself)}
/>
</.input_wrapper>
</div>
<%= submit("Add Hub",
class: "button-base button-blue",
phx_disable_with: "Add...",
disabled: not @changeset.valid?
) %>
<% end %>
</.form>
</div>
"""
end
@impl true
def handle_event("connect", _params, socket) do
url = get_field(socket.assigns.changeset, :url)
token = get_field(socket.assigns.changeset, :token)
with {:ok, _info} <- EnterpriseClient.fetch_info(url, token),
{:ok, %{"id" => id}} <- EnterpriseClient.fetch_me(url, token) do
base = %Enterprise{
token: token,
url: url,
external_id: id,
hub_name: "Enterprise",
hub_color: HexColor.random()
}
changeset = Enterprise.change_hub(base)
{:noreply, assign(socket, changeset: changeset, base: base, connected: true)}
else
{:error, _message, reason} ->
{:noreply,
socket
|> put_flash(:error, message_from_reason(reason))
|> push_patch(to: Routes.hub_path(socket, :new))}
end
end
def handle_event("randomize_color", _, socket) do
handle_event("validate", %{"enterprise" => %{"hub_color" => HexColor.random()}}, socket)
end
def handle_event("save", %{"enterprise" => params}, socket) do
if socket.assigns.changeset.valid? do
case Enterprise.create_hub(socket.assigns.base, params) do
{:ok, hub} ->
{:noreply,
socket
|> put_flash(:success, "Hub added successfully")
|> push_redirect(to: Routes.hub_path(socket, :edit, hub.id))}
{:error, changeset} ->
{:noreply, assign(socket, changeset: changeset)}
end
else
{:noreply, socket}
end
end
def handle_event("validate", %{"enterprise" => attrs}, socket) do
{:noreply, assign(socket, changeset: Enterprise.change_hub(socket.assigns.base, attrs))}
end
defp message_from_reason(:invalid_url), do: "Failed to connect with given URL"
defp message_from_reason(:unauthorized), do: "Failed to authorize with given token"
defp message_from_reason(:invalid_token), do: "Failed to authenticate with given token"
end

View file

@ -11,6 +11,9 @@ defmodule LivebookWeb.Hub.NewLive do
{:ok, assign(socket, selected_type: nil, page_title: "Livebook - Hub")}
end
@impl true
def handle_params(_params, _url, socket), do: {:noreply, socket}
@impl true
def render(assigns) do
~H"""
@ -69,13 +72,7 @@ defmodule LivebookWeb.Hub.NewLive do
<% end %>
<%= if @selected_type == "enterprise" do %>
<div>
Livebook Enterprise is currently in closed beta. If you want to learn more, <a
href="https://livebook.dev/#livebook-plans"
class="pointer-events-auto text-blue-600"
target="_blank"
>click here</a>.
</div>
<.live_component module={LivebookWeb.Hub.New.EnterpriseComponent} id="enterprise-form" />
<% end %>
</div>
<% end %>

View file

@ -0,0 +1,25 @@
defmodule Livebook.Hubs.EnterpriseClientTest do
use Livebook.EnterpriseIntegrationCase, async: true
alias Livebook.Hubs.EnterpriseClient
describe "fetch_info/1" do
test "fetches the token info", %{url: url, token: token} do
assert {:ok, %{"id" => _}} = EnterpriseClient.fetch_info(url, token)
end
test "returns invalid_token when token is invalid", %{url: url} do
assert {:error, _, :invalid_token} = EnterpriseClient.fetch_info(url, "foo")
end
end
describe "fetch_me/1" do
test "fetches the current user id", %{url: url, token: token} do
assert {:ok, %{"id" => _}} = EnterpriseClient.fetch_me(url, token)
end
test "returns unauthorized when token is invalid", %{url: url} do
assert {:error, _, :unauthorized} = EnterpriseClient.fetch_me(url, "foo")
end
end
end

View file

@ -10,13 +10,17 @@ defmodule LivebookWeb.Hub.EditLiveTest do
Hubs.clean_hubs()
end)
bypass = Bypass.open()
Application.put_env(:livebook, :fly_graphql_endpoint, "http://localhost:#{bypass.port}")
{:ok, bypass: bypass}
:ok
end
describe "fly" do
setup do
bypass = Bypass.open()
Application.put_env(:livebook, :fly_graphql_endpoint, "http://localhost:#{bypass.port}")
{:ok, bypass: bypass}
end
test "updates hub", %{conn: conn, bypass: bypass} do
{:ok, pid} = Agent.start(fn -> %{fun: &fetch_app_response/2, type: :mount} end)
@ -55,18 +59,7 @@ defmodule LivebookWeb.Hub.EditLiveTest do
assert render(view) =~ "Hub updated successfully"
assert view
|> element("#hubs")
|> render() =~ ~s/style="color: #FF00FF"/
assert view
|> element("#hubs")
|> render() =~ Routes.hub_path(conn, :edit, hub.id)
assert view
|> element("#hubs")
|> render() =~ "Personal Hub"
assert_hub(view, conn, %{hub | hub_color: attrs["hub_color"], hub_name: attrs["hub_name"]})
refute Hubs.fetch_hub!(hub.id) == hub
end
@ -197,6 +190,42 @@ defmodule LivebookWeb.Hub.EditLiveTest do
end
end
describe "enterprise" do
test "updates hub", %{conn: conn} do
hub = insert_hub(:enterprise)
{:ok, view, _html} = live(conn, Routes.hub_path(conn, :edit, hub.id))
attrs = %{"hub_color" => "#FF00FF"}
view
|> element("#enterprise-form")
|> render_change(%{"enterprise" => attrs})
refute view
|> element("#enterprise-form .invalid-feedback")
|> has_element?()
assert {:ok, view, _html} =
view
|> element("#enterprise-form")
|> render_submit(%{"enterprise" => attrs})
|> follow_redirect(conn)
assert render(view) =~ "Hub updated successfully"
assert_hub(view, conn, %{hub | hub_color: attrs["hub_color"]})
refute Hubs.fetch_hub!(hub.id) == hub
end
end
defp assert_hub(view, conn, hub) do
hubs_html = view |> element("#hubs") |> render()
assert hubs_html =~ ~s/style="color: #{hub.hub_color}"/
assert hubs_html =~ Routes.hub_path(conn, :edit, hub.id)
assert hubs_html =~ hub.hub_name
end
defp fly_bypass(bypass, app_id, agent_pid) do
Bypass.expect(bypass, "POST", "/", fn conn ->
{:ok, body, conn} = Plug.Conn.read_body(conn)

View file

@ -0,0 +1,114 @@
defmodule LivebookWeb.Hub.New.EnterpriseComponentTest do
use Livebook.EnterpriseIntegrationCase, async: true
import Phoenix.LiveViewTest
alias Livebook.Hubs
describe "enterprise" do
test "persists new hub", %{conn: conn, url: url, token: token} do
{:ok, view, _html} = live(conn, Routes.hub_path(conn, :new))
assert view
|> element("#enterprise")
|> render_click() =~ "2. Configure your Hub"
view
|> element("#enterprise-form")
|> render_change(%{
"enterprise" => %{
"url" => url,
"token" => token
}
})
assert view
|> element("#connect")
|> render_click() =~ "Add Hub"
attrs = %{
"url" => url,
"token" => token,
"hub_name" => "Enterprise",
"hub_color" => "#FF00FF"
}
view
|> element("#enterprise-form")
|> render_change(%{"enterprise" => attrs})
refute view
|> element("#enterprise-form .invalid-feedback")
|> has_element?()
result =
view
|> element("#enterprise-form")
|> render_submit(%{"enterprise" => attrs})
assert {:ok, view, _html} = follow_redirect(result, conn)
assert render(view) =~ "Hub added successfully"
hubs_html = view |> element("#hubs") |> render()
assert hubs_html =~ ~s/style="color: #FF00FF"/
assert hubs_html =~ "/hub/enterprise-bf1587a3-4501-4729-9f53-43679381e28b"
assert hubs_html =~ "Enterprise"
end
test "fails to create existing hub", %{conn: conn, url: url, token: token} do
hub =
insert_hub(:enterprise,
id: "enterprise-bf1587a3-4501-4729-9f53-43679381e28b",
external_id: "bf1587a3-4501-4729-9f53-43679381e28b",
url: url,
token: token
)
{:ok, view, _html} = live(conn, Routes.hub_path(conn, :new))
assert view
|> element("#enterprise")
|> render_click() =~ "2. Configure your Hub"
view
|> element("#enterprise-form")
|> render_change(%{
"enterprise" => %{
"url" => url,
"token" => token
}
})
assert view
|> element("#connect")
|> render_click() =~ "Add Hub"
attrs = %{
"url" => url,
"token" => token,
"hub_name" => "Enterprise",
"hub_color" => "#FFFFFF"
}
view
|> element("#enterprise-form")
|> render_change(%{"enterprise" => attrs})
refute view
|> element("#enterprise-form .invalid-feedback")
|> has_element?()
assert view
|> element("#enterprise-form")
|> render_submit(%{"enterprise" => attrs}) =~ "already exists"
hubs_html = view |> element("#hubs") |> render()
assert hubs_html =~ ~s/style="color: #{hub.hub_color}"/
assert hubs_html =~ Routes.hub_path(conn, :edit, hub.id)
assert hubs_html =~ hub.hub_name
assert Hubs.fetch_hub!(hub.id) == hub
end
end
end

View file

@ -18,7 +18,7 @@ defmodule LivebookWeb.Hub.NewLiveTest do
end
describe "fly" do
test "persists fly", %{conn: conn} do
test "persists new hub", %{conn: conn} do
fly_bypass("123456789")
{:ok, view, _html} = live(conn, Routes.hub_path(conn, :new))
@ -118,6 +118,124 @@ defmodule LivebookWeb.Hub.NewLiveTest do
end
end
describe "enterprise" do
test "persists new hub", %{conn: conn} do
id = Livebook.Utils.random_short_id()
bypass = enterprise_bypass(id)
{:ok, view, _html} = live(conn, Routes.hub_path(conn, :new))
assert view
|> element("#enterprise")
|> render_click() =~ "2. Configure your Hub"
view
|> element("#enterprise-form")
|> render_change(%{
"enterprise" => %{
"url" => "http://localhost:#{bypass.port}",
"token" => "dummy access token"
}
})
assert view
|> element("#connect")
|> render_click() =~ "Add Hub"
attrs = %{
"url" => "http://localhost:#{bypass.port}",
"token" => "dummy access token",
"hub_name" => "Enterprise",
"hub_color" => "#FF00FF"
}
view
|> element("#enterprise-form")
|> render_change(%{"enterprise" => attrs})
refute view
|> element("#enterprise-form .invalid-feedback")
|> has_element?()
assert {:ok, view, _html} =
view
|> element("#enterprise-form")
|> render_submit(%{"enterprise" => attrs})
|> follow_redirect(conn)
assert render(view) =~ "Hub added successfully"
assert view
|> element("#hubs")
|> render() =~ ~s/style="color: #FF00FF"/
assert view
|> element("#hubs")
|> render() =~ "/hub/enterprise-#{id}"
assert view
|> element("#hubs")
|> render() =~ "Enterprise"
end
test "fails to create existing hub", %{conn: conn} do
hub = insert_hub(:enterprise, id: "enterprise-foo", external_id: "foo")
bypass = enterprise_bypass(hub.external_id)
{:ok, view, _html} = live(conn, Routes.hub_path(conn, :new))
assert view
|> element("#enterprise")
|> render_click() =~ "2. Configure your Hub"
view
|> element("#enterprise-form")
|> render_change(%{
"enterprise" => %{
"url" => "http://localhost:#{bypass.port}",
"token" => "dummy access token"
}
})
assert view
|> element("#connect")
|> render_click() =~ "Add Hub"
attrs = %{
"url" => "http://localhost:#{bypass.port}",
"token" => "dummy access token",
"hub_name" => "Enterprise",
"hub_color" => "#FF00FF"
}
view
|> element("#enterprise-form")
|> render_change(%{"enterprise" => attrs})
refute view
|> element("#enterprise-form .invalid-feedback")
|> has_element?()
assert view
|> element("#enterprise-form")
|> render_submit(%{"enterprise" => attrs}) =~ "already exists"
assert view
|> element("#hubs")
|> render() =~ ~s/style="color: #{hub.hub_color}"/
assert view
|> element("#hubs")
|> render() =~ Routes.hub_path(conn, :edit, hub.id)
assert view
|> element("#hubs")
|> render() =~ hub.hub_name
assert Hubs.fetch_hub!(hub.id) == hub
end
end
defp fly_bypass(app_id) do
bypass = Bypass.open()
Application.put_env(:livebook, :fly_graphql_endpoint, "http://localhost:#{bypass.port}")
@ -138,6 +256,37 @@ defmodule LivebookWeb.Hub.NewLiveTest do
end)
end
defp enterprise_bypass(id) do
bypass = Bypass.open()
Bypass.expect(bypass, "POST", "/api/v1", fn conn ->
{:ok, body, conn} = Plug.Conn.read_body(conn)
body = Jason.decode!(body)
response =
cond do
body["query"] =~ "info" ->
%{
"data" => %{
"info" => %{
"id" => Livebook.Utils.random_short_id(),
"expire_at" => to_string(DateTime.utc_now())
}
}
}
body["query"] =~ "me" ->
%{"data" => %{"me" => %{"id" => id}}}
end
conn
|> Plug.Conn.put_resp_content_type("application/json")
|> Plug.Conn.resp(200, Jason.encode!(response))
end)
bypass
end
defp fetch_apps_response(app_id) do
app = %{
"id" => app_id,

View file

@ -0,0 +1,21 @@
defmodule Livebook.EnterpriseIntegrationCase do
use ExUnit.CaseTemplate
alias Livebook.EnterpriseServer
using do
quote do
use LivebookWeb.ConnCase
@moduletag :enterprise_integration
alias Livebook.EnterpriseServer
end
end
setup_all do
EnterpriseServer.start()
{:ok, url: EnterpriseServer.url(), token: EnterpriseServer.token()}
end
end

View file

@ -31,6 +31,30 @@ defmodule Livebook.Factory do
}
end
def build(:enterprise_metadata) do
id = Livebook.Utils.random_short_id()
%Livebook.Hubs.Metadata{
id: "enterprise-#{id}",
name: "Enterprise",
color: "#FF00FF",
provider: build(:enterprise)
}
end
def build(:enterprise) do
id = Livebook.Utils.random_short_id()
%Livebook.Hubs.Enterprise{
id: "enterprise-#{id}",
hub_name: "Enterprise",
hub_color: "#FF0000",
external_id: id,
token: Livebook.Utils.random_cookie(),
url: "http://localhost"
}
end
def build(:env_var) do
%Livebook.Settings.EnvVar{
name: "BAR",

View file

@ -0,0 +1,189 @@
defmodule Livebook.EnterpriseServer do
@moduledoc false
use GenServer
@name __MODULE__
def start do
GenServer.start(__MODULE__, [], name: @name)
end
def url do
"http://localhost:#{app_port()}"
end
def token do
GenServer.call(@name, :fetch_token)
end
# GenServer Callbacks
@impl true
def init(_opts) do
{:ok, %{token: nil, node: enterprise_node(), port: nil}, {:continue, :start_enterprise}}
end
@impl true
def handle_continue(:start_enterprise, state) do
{:noreply, %{state | port: start_enterprise(state)}}
end
@impl true
def handle_call(:fetch_token, _from, state) do
token = state.token || fetch_token_from_enterprise(state)
{:reply, token, %{state | token: token}}
end
# Port Callbacks
@impl true
def handle_info({_port, {:data, message}}, state) do
info(message)
{:noreply, state}
end
def handle_info({_port, {:exit_status, status}}, _state) do
error("enterprise quit with status #{status}")
System.halt(status)
end
# Private
defp fetch_token_from_enterprise(state) do
:erpc.call(state.node, Enterprise.Integration, :fetch_token, [])
end
defp start_enterprise(state) do
ensure_app_dir!()
prepare_database()
env = [
{~c"MIX_ENV", ~c"livebook"},
{~c"LIVEBOOK_ENTERPRISE_PORT", String.to_charlist(app_port())}
]
args = [
"-e",
"spawn(fn -> IO.gets([]) && System.halt(0) end)",
"--sname",
to_string(state.node),
"--cookie",
to_string(Node.get_cookie()),
"-S",
"mix",
"phx.server"
]
port =
Port.open({:spawn_executable, elixir_executable()}, [
:exit_status,
:use_stdio,
:stderr_to_stdout,
:binary,
:hide,
env: env,
cd: app_dir(),
args: args
])
wait_on_start(port)
end
defp prepare_database do
mix(["ecto.drop", "--quiet"])
mix(["ecto.create", "--quiet"])
mix(["ecto.migrate", "--quiet"])
end
defp ensure_app_dir! do
dir = app_dir()
unless File.exists?(dir) do
IO.puts(
"Unable to find #{dir}, make sure to clone the enterprise repository " <>
"into it to run integration tests or set ENTERPRISE_PATH to its location"
)
System.halt(1)
end
end
defp app_dir do
System.get_env("ENTERPRISE_PATH", "../enterprise")
end
defp app_port do
System.get_env("ENTERPRISE_PORT", "4043")
end
defp wait_on_start(port) do
case :httpc.request(:get, {~c"#{url()}/public/health", []}, [], []) do
{:ok, _} ->
port
{:error, _} ->
Process.sleep(10)
wait_on_start(port)
end
end
defp mix(args, opts \\ []) do
env = [
{"MIX_ENV", "livebook"},
{"LIVEBOOK_ENTERPRISE_PORT", app_port()}
]
cmd_opts = [stderr_to_stdout: true, env: env, cd: app_dir()]
args = ["--erl", "-elixir ansi_enabled true", "-S", "mix" | args]
cmd_opts =
if opts[:with_return],
do: cmd_opts,
else: Keyword.put(cmd_opts, :into, IO.stream(:stdio, :line))
if opts[:with_return] do
case System.cmd(elixir_executable(), args, cmd_opts) do
{result, 0} ->
result
{message, status} ->
error("""
#{message}\
""")
System.halt(status)
end
else
{_, 0} = System.cmd(elixir_executable(), args, cmd_opts)
end
end
defp elixir_executable do
System.find_executable("elixir")
end
defp enterprise_node do
:"enterprise_#{Livebook.Utils.random_short_id()}@#{hostname()}"
end
defp hostname do
[nodename, hostname] =
node()
|> Atom.to_charlist()
|> :string.split(~c"@")
with {:ok, nodenames} <- :erl_epmd.names(hostname),
true <- List.keymember?(nodenames, nodename, 0) do
hostname
else
_ ->
raise "Error"
end
end
defp info(message), do: log([:blue, message <> "\n"])
defp error(message), do: log([:red, message <> "\n"])
defp log(data), do: data |> IO.ANSI.format() |> IO.write()
end

View file

@ -41,7 +41,10 @@ Livebook.Storage.insert(:settings, "global", autosave_path: nil)
erl_docs_available? = Code.fetch_docs(:gen_server) != {:error, :chunk_not_found}
exclude = []
exclude = if erl_docs_available?, do: exclude, else: Keyword.put(exclude, :erl_docs, true)
enterprise_path = System.get_env("ENTERPRISE_PATH", "../enterprise")
enterprise_available? = File.exists?(enterprise_path)
ExUnit.start(assert_receive_timeout: 1_500, exclude: exclude)
ExUnit.start(
assert_receive_timeout: 1_500,
exclude: [erl_docs: erl_docs_available?, enterprise_integration: !enterprise_available?]
)