mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-01-30 10:47:51 +08:00
Improve Add Hub and Edit Hub pages (#1929)
This commit is contained in:
parent
683cd8a0d1
commit
76015e3009
29 changed files with 264 additions and 2129 deletions
|
@ -49,8 +49,13 @@ export function registerGlobalEventHandlers() {
|
|||
|
||||
window.addEventListener("lb:clipcopy", (event) => {
|
||||
if ("clipboard" in navigator) {
|
||||
const text = event.target.textContent;
|
||||
navigator.clipboard.writeText(text);
|
||||
const tag = event.target.tagName;
|
||||
|
||||
if (tag === "INPUT") {
|
||||
navigator.clipboard.writeText(event.target.value);
|
||||
} else {
|
||||
navigator.clipboard.writeText(event.target.tagName);
|
||||
}
|
||||
} else {
|
||||
alert(
|
||||
"Sorry, your browser does not support clipboard copy.\nThis generally requires a secure origin — either HTTPS or localhost."
|
||||
|
|
|
@ -58,7 +58,7 @@ config :livebook, LivebookWeb.Endpoint,
|
|||
live_reload: [
|
||||
patterns: [
|
||||
~r"tmp/static_dev/.*(js|css|png|jpeg|jpg|gif|svg)$",
|
||||
~r"lib/livebook_web/(live|views)/.*(ex)$",
|
||||
~r"lib/livebook_web/(live|views|components)/.*(ex)$",
|
||||
~r"lib/livebook_web/templates/.*(eex)$"
|
||||
]
|
||||
]
|
||||
|
|
|
@ -2,7 +2,7 @@ defmodule Livebook.Hubs do
|
|||
@moduledoc false
|
||||
|
||||
alias Livebook.Storage
|
||||
alias Livebook.Hubs.{Broadcasts, Enterprise, Fly, Metadata, Personal, Provider, Team}
|
||||
alias Livebook.Hubs.{Broadcasts, Metadata, Personal, Provider, Team}
|
||||
alias Livebook.Secrets.Secret
|
||||
|
||||
@namespace :hubs
|
||||
|
@ -157,14 +157,6 @@ defmodule Livebook.Hubs do
|
|||
Phoenix.PubSub.unsubscribe(Livebook.PubSub, "hubs:#{topic}")
|
||||
end
|
||||
|
||||
defp to_struct(%{id: "fly-" <> _} = fields) do
|
||||
Provider.load(%Fly{}, fields)
|
||||
end
|
||||
|
||||
defp to_struct(%{id: "enterprise-" <> _} = fields) do
|
||||
Provider.load(%Enterprise{}, fields)
|
||||
end
|
||||
|
||||
defp to_struct(%{id: "personal-" <> _} = fields) do
|
||||
Provider.load(%Personal{}, fields)
|
||||
end
|
||||
|
|
|
@ -1,203 +0,0 @@
|
|||
defmodule Livebook.Hubs.Enterprise do
|
||||
@moduledoc false
|
||||
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
alias Livebook.Hubs
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
id: String.t() | nil,
|
||||
org_id: pos_integer() | nil,
|
||||
user_id: pos_integer() | nil,
|
||||
org_key_id: pos_integer() | nil,
|
||||
teams_key: String.t() | nil,
|
||||
session_token: String.t() | nil,
|
||||
hub_name: String.t() | nil,
|
||||
hub_emoji: String.t() | nil
|
||||
}
|
||||
|
||||
embedded_schema do
|
||||
field :org_id, :integer
|
||||
field :user_id, :integer
|
||||
field :org_key_id, :integer
|
||||
field :teams_key, :string
|
||||
field :session_token, :string
|
||||
field :hub_name, :string
|
||||
field :hub_emoji, :string
|
||||
end
|
||||
|
||||
@fields ~w(
|
||||
org_id
|
||||
user_id
|
||||
org_key_id
|
||||
teams_key
|
||||
session_token
|
||||
hub_name
|
||||
hub_emoji
|
||||
)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
|
||||
changeset(enterprise, attrs)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns changeset with applied validations.
|
||||
"""
|
||||
@spec validate_hub(t(), map()) :: Ecto.Changeset.t()
|
||||
def validate_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(:hub_name, "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(:hub_name, "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, :hub_name) do
|
||||
nil -> changeset
|
||||
hub_name -> put_change(changeset, :id, "enterprise-#{hub_name}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Enterprise do
|
||||
alias Livebook.Hubs.EnterpriseClient
|
||||
|
||||
def load(enterprise, fields) do
|
||||
%{
|
||||
enterprise
|
||||
| id: fields.id,
|
||||
session_token: fields.session_token,
|
||||
teams_key: fields.teams_key,
|
||||
org_id: fields.org_id,
|
||||
user_id: fields.user_id,
|
||||
org_key_id: fields.org_key_id,
|
||||
hub_name: fields.hub_name,
|
||||
hub_emoji: fields.hub_emoji
|
||||
}
|
||||
end
|
||||
|
||||
def to_metadata(enterprise) do
|
||||
%Livebook.Hubs.Metadata{
|
||||
id: enterprise.id,
|
||||
name: enterprise.hub_name,
|
||||
provider: enterprise,
|
||||
emoji: enterprise.hub_emoji,
|
||||
connected?: EnterpriseClient.connected?(enterprise.id)
|
||||
}
|
||||
end
|
||||
|
||||
def type(_enterprise), do: "enterprise"
|
||||
|
||||
def connection_spec(enterprise), do: {EnterpriseClient, enterprise}
|
||||
|
||||
def disconnect(enterprise) do
|
||||
EnterpriseClient.stop(enterprise.id)
|
||||
end
|
||||
|
||||
def capabilities(_enterprise), do: ~w(connect list_secrets create_secret)a
|
||||
|
||||
def get_secrets(enterprise) do
|
||||
EnterpriseClient.get_secrets(enterprise.id)
|
||||
end
|
||||
|
||||
def create_secret(enterprise, secret) do
|
||||
data = LivebookProto.build_create_secret_request(name: secret.name, value: secret.value)
|
||||
|
||||
case EnterpriseClient.send_request(enterprise.id, data) do
|
||||
{:create_secret, _} ->
|
||||
:ok
|
||||
|
||||
{:changeset_error, errors} ->
|
||||
changeset =
|
||||
for {field, messages} <- errors,
|
||||
message <- messages,
|
||||
reduce: secret do
|
||||
acc ->
|
||||
Livebook.Secrets.add_secret_error(acc, field, message)
|
||||
end
|
||||
|
||||
{:error, changeset}
|
||||
|
||||
{:transport_error, reason} ->
|
||||
message = "#{enterprise.hub_emoji} #{enterprise.hub_name}: #{reason}"
|
||||
changeset = Livebook.Secrets.add_secret_error(secret, :hub_id, message)
|
||||
|
||||
{:error, changeset}
|
||||
end
|
||||
end
|
||||
|
||||
def update_secret(_enterprise, _secret), do: raise("not implemented")
|
||||
|
||||
def delete_secret(_enterprise, _secret), do: raise("not implemented")
|
||||
|
||||
def connection_error(enterprise) do
|
||||
reason = EnterpriseClient.get_connection_error(enterprise.id)
|
||||
"Cannot connect to Hub: #{reason}. Will attempt to reconnect automatically..."
|
||||
end
|
||||
|
||||
# TODO: implement signing through the enterprise server
|
||||
def notebook_stamp(_hub, _notebook_source, _metadata) do
|
||||
:skip
|
||||
end
|
||||
|
||||
def verify_notebook_stamp(_hub, _notebook_source, _stamp), do: raise("not implemented")
|
||||
end
|
|
@ -1,225 +0,0 @@
|
|||
defmodule Livebook.Hubs.EnterpriseClient do
|
||||
@moduledoc false
|
||||
use GenServer
|
||||
require Logger
|
||||
|
||||
alias Livebook.Hubs.Broadcasts
|
||||
alias Livebook.Hubs.Enterprise
|
||||
alias Livebook.Secrets.Secret
|
||||
alias Livebook.WebSocket.ClientConnection
|
||||
|
||||
@registry Livebook.HubsRegistry
|
||||
@supervisor Livebook.HubsSupervisor
|
||||
|
||||
defstruct [:server, :hub, :connection_error, connected?: false, secrets: []]
|
||||
|
||||
@type registry_name :: {:via, Registry, {Livebook.HubsRegistry, String.t()}}
|
||||
|
||||
@doc """
|
||||
Connects the Enterprise client with WebSocket server.
|
||||
"""
|
||||
@spec start_link(Enterprise.t()) :: GenServer.on_start()
|
||||
def start_link(%Enterprise{} = enterprise) do
|
||||
GenServer.start_link(__MODULE__, enterprise, name: registry_name(enterprise.id))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Stops the WebSocket server.
|
||||
"""
|
||||
@spec stop(String.t()) :: :ok
|
||||
def stop(id) do
|
||||
if pid = GenServer.whereis(registry_name(id)) do
|
||||
DynamicSupervisor.terminate_child(@supervisor, pid)
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sends a request to the WebSocket server.
|
||||
"""
|
||||
@spec send_request(String.t() | registry_name() | pid(), WebSocket.proto()) :: {atom(), term()}
|
||||
def send_request(id, %_struct{} = data) when is_binary(id) do
|
||||
send_request(registry_name(id), data)
|
||||
end
|
||||
|
||||
def send_request(pid, %_struct{} = data) do
|
||||
with {:ok, server} <- GenServer.call(pid, :fetch_server) do
|
||||
ClientConnection.send_request(server, data)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list of cached secrets.
|
||||
"""
|
||||
@spec get_secrets(String.t()) :: list(Secret.t())
|
||||
def get_secrets(id) do
|
||||
GenServer.call(registry_name(id), :get_secrets)
|
||||
catch
|
||||
:exit, _ -> []
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the latest error from connection.
|
||||
"""
|
||||
@spec get_connection_error(String.t()) :: String.t() | nil
|
||||
def get_connection_error(id) do
|
||||
GenServer.call(registry_name(id), :get_connection_error)
|
||||
catch
|
||||
:exit, _ -> "connection refused"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns if the given enterprise is connected.
|
||||
"""
|
||||
@spec connected?(String.t()) :: boolean()
|
||||
def connected?(id) do
|
||||
GenServer.call(registry_name(id), :connected?)
|
||||
catch
|
||||
:exit, _ -> false
|
||||
end
|
||||
|
||||
## GenServer callbacks
|
||||
|
||||
@impl true
|
||||
def init(%Enterprise{} = enterprise) do
|
||||
# TODO: Make it work with new struct and `Livebook.Teams`
|
||||
{:ok, pid} = ClientConnection.start_link(self(), Livebook.Config.teams_url())
|
||||
|
||||
{:ok, %__MODULE__{hub: enterprise, server: pid}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_continue(:synchronize_user, state) do
|
||||
data = LivebookProto.build_handshake_request(app_version: Livebook.Config.app_version())
|
||||
{:handshake, _} = ClientConnection.send_request(state.server, data)
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:fetch_server, _caller, state) do
|
||||
if state.connected? do
|
||||
{:reply, {:ok, state.server}, state}
|
||||
else
|
||||
{:reply, {:transport_error, state.connection_error}, state}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_call(:get_secrets, _caller, state) do
|
||||
{:reply, state.secrets, state}
|
||||
end
|
||||
|
||||
def handle_call(:get_connection_error, _caller, state) do
|
||||
{:reply, state.connection_error, state}
|
||||
end
|
||||
|
||||
def handle_call(:connected?, _caller, state) do
|
||||
{:reply, state.connected?, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:connect, :ok, _}, state) do
|
||||
Broadcasts.hub_connected()
|
||||
{:noreply, %{state | connected?: true, connection_error: nil}, {:continue, :synchronize_user}}
|
||||
end
|
||||
|
||||
def handle_info({:connect, :error, reason}, state) do
|
||||
Broadcasts.hub_connection_failed(reason)
|
||||
{:noreply, %{state | connected?: false, connection_error: reason}}
|
||||
end
|
||||
|
||||
def handle_info({:disconnect, :ok, :disconnected}, state) do
|
||||
Broadcasts.hub_disconnected()
|
||||
{:stop, :normal, state}
|
||||
end
|
||||
|
||||
def handle_info({:event, topic, data}, state) do
|
||||
Logger.debug("Received event #{topic} with data: #{inspect(data)}")
|
||||
|
||||
{:noreply, handle_event(topic, data, state)}
|
||||
end
|
||||
|
||||
# Private
|
||||
|
||||
defp registry_name(id) do
|
||||
{:via, Registry, {@registry, id}}
|
||||
end
|
||||
|
||||
defp put_secret(state, secret) do
|
||||
state = remove_secret(state, secret)
|
||||
%{state | secrets: [secret | state.secrets]}
|
||||
end
|
||||
|
||||
defp remove_secret(state, secret) do
|
||||
%{state | secrets: Enum.reject(state.secrets, &(&1.name == secret.name))}
|
||||
end
|
||||
|
||||
defp build_secret(state, %{name: name, value: value}),
|
||||
do: %Secret{name: name, value: value, hub_id: state.hub.id, readonly: true}
|
||||
|
||||
defp update_hub(state, name) do
|
||||
case Enterprise.update_hub(state.hub, %{hub_name: name}) do
|
||||
{:ok, hub} -> %{state | hub: hub}
|
||||
{:error, _} -> state
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_event(:secret_created, secret_created, state) do
|
||||
secret = build_secret(state, secret_created)
|
||||
Broadcasts.secret_created(secret)
|
||||
|
||||
put_secret(state, secret)
|
||||
end
|
||||
|
||||
defp handle_event(:secret_updated, secret_updated, state) do
|
||||
secret = build_secret(state, secret_updated)
|
||||
Broadcasts.secret_updated(secret)
|
||||
|
||||
put_secret(state, secret)
|
||||
end
|
||||
|
||||
defp handle_event(:secret_deleted, secret_deleted, state) do
|
||||
secret = build_secret(state, secret_deleted)
|
||||
Broadcasts.secret_deleted(secret)
|
||||
|
||||
remove_secret(state, secret)
|
||||
end
|
||||
|
||||
defp handle_event(:user_synchronized, user_synchronized, %{secrets: []} = state) do
|
||||
state = update_hub(state, user_synchronized.name)
|
||||
secrets = for secret <- user_synchronized.secrets, do: build_secret(state, secret)
|
||||
|
||||
%{state | secrets: secrets}
|
||||
end
|
||||
|
||||
defp handle_event(:user_synchronized, user_synchronized, state) do
|
||||
state = update_hub(state, user_synchronized.name)
|
||||
secrets = for secret <- user_synchronized.secrets, do: build_secret(state, secret)
|
||||
|
||||
created_secrets =
|
||||
Enum.reject(secrets, fn secret ->
|
||||
Enum.find(state.secrets, &(&1.name == secret.name and &1.value == secret.value))
|
||||
end)
|
||||
|
||||
deleted_secrets =
|
||||
Enum.reject(state.secrets, fn secret ->
|
||||
Enum.find(secrets, &(&1.name == secret.name))
|
||||
end)
|
||||
|
||||
updated_secrets =
|
||||
Enum.filter(secrets, fn secret ->
|
||||
Enum.find(state.secrets, &(&1.name == secret.name and &1.value != secret.value))
|
||||
end)
|
||||
|
||||
events_by_type = [
|
||||
secret_deleted: deleted_secrets,
|
||||
secret_created: created_secrets,
|
||||
secret_updated: updated_secrets
|
||||
]
|
||||
|
||||
for {type, events} <- events_by_type, event <- events, reduce: state do
|
||||
state -> handle_event(type, event, state)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,171 +0,0 @@
|
|||
defmodule Livebook.Hubs.Fly do
|
||||
@moduledoc false
|
||||
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
alias Livebook.Hubs
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
id: String.t() | nil,
|
||||
access_token: String.t() | nil,
|
||||
hub_name: String.t() | nil,
|
||||
hub_emoji: String.t() | nil,
|
||||
organization_id: String.t() | nil,
|
||||
organization_type: String.t() | nil,
|
||||
organization_name: String.t() | nil,
|
||||
application_id: String.t() | nil
|
||||
}
|
||||
|
||||
embedded_schema do
|
||||
field :access_token, :string
|
||||
field :hub_name, :string
|
||||
field :hub_emoji, :string
|
||||
field :organization_id, :string
|
||||
field :organization_type, :string
|
||||
field :organization_name, :string
|
||||
field :application_id, :string
|
||||
end
|
||||
|
||||
@fields ~w(
|
||||
access_token
|
||||
hub_name
|
||||
hub_emoji
|
||||
organization_id
|
||||
organization_name
|
||||
organization_type
|
||||
application_id
|
||||
)a
|
||||
|
||||
@doc """
|
||||
Returns an `%Ecto.Changeset{}` for tracking hub changes.
|
||||
"""
|
||||
@spec change_hub(t(), map()) :: Ecto.Changeset.t()
|
||||
def change_hub(%__MODULE__{} = fly, attrs \\ %{}) do
|
||||
changeset(fly, attrs)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns changeset with applied validations.
|
||||
"""
|
||||
@spec validate_hub(t(), map()) :: Ecto.Changeset.t()
|
||||
def validate_hub(%__MODULE__{} = fly, attrs \\ %{}) do
|
||||
fly
|
||||
|> 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__{} = fly, attrs) do
|
||||
changeset = changeset(fly, attrs)
|
||||
|
||||
if Hubs.hub_exists?(fly.id) do
|
||||
{:error,
|
||||
changeset
|
||||
|> add_error(:application_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__{} = fly, attrs) do
|
||||
changeset = changeset(fly, attrs)
|
||||
|
||||
if Hubs.hub_exists?(fly.id) do
|
||||
with {:ok, struct} <- apply_action(changeset, :update) do
|
||||
Hubs.save_hub(struct)
|
||||
{:ok, struct}
|
||||
end
|
||||
else
|
||||
{:error,
|
||||
changeset
|
||||
|> add_error(:application_id, "does not exists")
|
||||
|> Map.replace!(:action, :validate)}
|
||||
end
|
||||
end
|
||||
|
||||
defp changeset(fly, attrs) do
|
||||
fly
|
||||
|> cast(attrs, @fields)
|
||||
|> validate_required(@fields)
|
||||
|> add_id()
|
||||
end
|
||||
|
||||
defp add_id(changeset) do
|
||||
if application_id = get_field(changeset, :application_id) do
|
||||
change(changeset, %{id: "fly-#{application_id}"})
|
||||
else
|
||||
changeset
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Fly do
|
||||
def load(fly, fields) do
|
||||
%{
|
||||
fly
|
||||
| id: fields.id,
|
||||
access_token: fields.access_token,
|
||||
hub_name: fields.hub_name,
|
||||
hub_emoji: fields.hub_emoji,
|
||||
organization_id: fields.organization_id,
|
||||
organization_type: fields.organization_type,
|
||||
organization_name: fields.organization_name,
|
||||
application_id: fields.application_id
|
||||
}
|
||||
end
|
||||
|
||||
def to_metadata(fly) do
|
||||
%Livebook.Hubs.Metadata{
|
||||
id: fly.id,
|
||||
name: fly.hub_name,
|
||||
provider: fly,
|
||||
emoji: fly.hub_emoji,
|
||||
connected?: false
|
||||
}
|
||||
end
|
||||
|
||||
def type(_fly), do: "fly"
|
||||
|
||||
def connection_spec(_fly), do: nil
|
||||
|
||||
def disconnect(_fly), do: raise("not implemented")
|
||||
|
||||
def capabilities(_fly), do: []
|
||||
|
||||
def get_secrets(_fly), do: []
|
||||
|
||||
# TODO: Implement the FlyClient.set_secrets/2
|
||||
def create_secret(_fly, _secret), do: :ok
|
||||
|
||||
# TODO: Implement the FlyClient.set_secrets/2
|
||||
def update_secret(_fly, _secret), do: :ok
|
||||
|
||||
# TODO: Implement the FlyClient.unset_secrets/2
|
||||
def delete_secret(_fly, _secret), do: :ok
|
||||
|
||||
def connection_error(_fly), do: raise("not implemented")
|
||||
|
||||
def notebook_stamp(_hub, _notebook_source, _metadata) do
|
||||
:skip
|
||||
end
|
||||
|
||||
def verify_notebook_stamp(_hub, _notebook_source, _stamp), do: raise("not implemented")
|
||||
end
|
|
@ -1,139 +0,0 @@
|
|||
defmodule Livebook.Hubs.FlyClient do
|
||||
@moduledoc false
|
||||
|
||||
alias Livebook.Hubs.Fly
|
||||
alias Livebook.Utils.HTTP
|
||||
|
||||
def fetch_apps(access_token) do
|
||||
query = """
|
||||
query {
|
||||
apps {
|
||||
nodes {
|
||||
id
|
||||
organization {
|
||||
id
|
||||
name
|
||||
type
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
with {:ok, %{"apps" => %{"nodes" => nodes}}} <- graphql(access_token, query) do
|
||||
apps =
|
||||
for node <- nodes do
|
||||
%Fly{
|
||||
id: "fly-" <> node["id"],
|
||||
access_token: access_token,
|
||||
organization_id: node["organization"]["id"],
|
||||
organization_type: node["organization"]["type"],
|
||||
organization_name: node["organization"]["name"],
|
||||
application_id: node["id"]
|
||||
}
|
||||
end
|
||||
|
||||
{:ok, apps}
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_app(%Fly{application_id: app_id, access_token: access_token}) do
|
||||
query = """
|
||||
query($appId: String!) {
|
||||
app(id: $appId) {
|
||||
id
|
||||
name
|
||||
hostname
|
||||
platformVersion
|
||||
deployed
|
||||
status
|
||||
secrets {
|
||||
id
|
||||
name
|
||||
digest
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
with {:ok, %{"app" => app}} <- graphql(access_token, query, %{appId: app_id}) do
|
||||
{:ok, app}
|
||||
end
|
||||
end
|
||||
|
||||
def set_secrets(%Fly{access_token: access_token, application_id: application_id}, secrets) do
|
||||
mutation = """
|
||||
mutation($input: SetSecretsInput!) {
|
||||
setSecrets(input: $input) {
|
||||
app {
|
||||
secrets {
|
||||
id
|
||||
name
|
||||
digest
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
input = %{input: %{appId: application_id, secrets: secrets}}
|
||||
|
||||
with {:ok, %{"setSecrets" => %{"app" => app}}} <- graphql(access_token, mutation, input) do
|
||||
{:ok, app["secrets"]}
|
||||
end
|
||||
end
|
||||
|
||||
def unset_secrets(%Fly{access_token: access_token, application_id: application_id}, keys) do
|
||||
mutation = """
|
||||
mutation($input: UnsetSecretsInput!) {
|
||||
unsetSecrets(input: $input) {
|
||||
app {
|
||||
secrets {
|
||||
id
|
||||
name
|
||||
digest
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
input = %{input: %{appId: application_id, keys: keys}}
|
||||
|
||||
with {:ok, %{"unsetSecrets" => %{"app" => app}}} <- graphql(access_token, mutation, input) do
|
||||
{:ok, app["secrets"]}
|
||||
end
|
||||
end
|
||||
|
||||
defp graphql(access_token, query, input \\ %{}) do
|
||||
headers = [{"Authorization", "Bearer #{access_token}"}]
|
||||
body = {"application/json", Jason.encode!(%{query: query, variables: input})}
|
||||
|
||||
case HTTP.request(:post, graphql_endpoint(), headers: headers, body: body) do
|
||||
{:ok, 200, _, body} ->
|
||||
case Jason.decode!(body) do
|
||||
%{"errors" => [%{"extensions" => %{"code" => code}}]} ->
|
||||
{:error, "request failed with code: #{code}"}
|
||||
|
||||
%{"errors" => [%{"message" => message}]} ->
|
||||
{:error, message}
|
||||
|
||||
%{"data" => data} ->
|
||||
{:ok, data}
|
||||
end
|
||||
|
||||
{:ok, _, _, body} ->
|
||||
{:error, body}
|
||||
|
||||
{:error, _} = error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
defp graphql_endpoint do
|
||||
Application.get_env(:livebook, :fly_graphql_endpoint, "https://api.fly.io/graphql")
|
||||
end
|
||||
end
|
|
@ -1,23 +1,32 @@
|
|||
defmodule LivebookWeb.Hub.Edit.PersonalComponent do
|
||||
use LivebookWeb, :live_component
|
||||
|
||||
alias Livebook.Hubs
|
||||
alias Livebook.Hubs.Personal
|
||||
alias LivebookWeb.LayoutHelpers
|
||||
|
||||
@impl true
|
||||
def update(assigns, socket) do
|
||||
socket = assign(socket, assigns)
|
||||
changeset = Personal.change_hub(assigns.hub)
|
||||
secrets = Hubs.get_secrets(assigns.hub)
|
||||
secret_name = assigns.params["secret_name"]
|
||||
|
||||
secret_value =
|
||||
if assigns.live_action == :edit_secret do
|
||||
secret = Enum.find(assigns.secrets, &(&1.name == assigns.secret_name))
|
||||
secret.value
|
||||
secrets
|
||||
|> Enum.find(&(&1.name == secret_name))
|
||||
|> Map.get(:value)
|
||||
end
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(assigns)
|
||||
|> assign(changeset: changeset, stamp_changeset: changeset, secret_value: secret_value)}
|
||||
assign(socket,
|
||||
secrets: secrets,
|
||||
changeset: changeset,
|
||||
stamp_changeset: changeset,
|
||||
secret_name: secret_name,
|
||||
secret_value: secret_value
|
||||
)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
|
|
@ -7,52 +7,141 @@ defmodule LivebookWeb.Hub.Edit.TeamComponent do
|
|||
|
||||
@impl true
|
||||
def update(assigns, socket) do
|
||||
socket = assign(socket, assigns)
|
||||
changeset = Team.change_hub(assigns.hub)
|
||||
show_key? = assigns.params["show-key"] == "true"
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(assigns)
|
||||
|> assign(show_key: show_key?)
|
||||
|> assign_form(changeset)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div id={"#{@id}-component"} class="space-y-8">
|
||||
<div class="flex relative">
|
||||
<LayoutHelpers.title text={"#{@hub.hub_emoji} #{@hub.hub_name}"} />
|
||||
<div id={"#{@id}-component"}>
|
||||
<.modal
|
||||
:if={@show_key}
|
||||
id="show-key-modal"
|
||||
width={:medium}
|
||||
show={true}
|
||||
patch={~p"/hub/#{@hub.id}"}
|
||||
>
|
||||
<div class="p-6 flex flex-col space-y-5">
|
||||
<h3 class="text-2xl font-semibold text-gray-800">
|
||||
Teams Key
|
||||
</h3>
|
||||
<div class="justify-center">
|
||||
This is your <strong>Teams Key</strong>. If you want to join or invite others
|
||||
to your organization, you will need to share your Teams Key with them. We
|
||||
recommend storing it somewhere safe:
|
||||
</div>
|
||||
<div class=" w-full">
|
||||
<div id="teams-key-toggle" class="relative flex">
|
||||
<input
|
||||
type="password"
|
||||
id="teams-key"
|
||||
readonly
|
||||
value={@hub.teams_key}
|
||||
class="input font-mono w-full border-neutral-200 bg-neutral-100 py-2 border-2 pr-8"
|
||||
/>
|
||||
|
||||
<button
|
||||
phx-click={JS.push("delete_hub", value: %{id: @hub.id})}
|
||||
class="absolute right-0 button-base button-red"
|
||||
>
|
||||
Delete hub
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center absolute inset-y-0 right-1">
|
||||
<button
|
||||
class="icon-button"
|
||||
data-copy
|
||||
data-tooltip="Copied to clipboard"
|
||||
type="button"
|
||||
aria-label="copy to clipboard"
|
||||
phx-click={
|
||||
JS.dispatch("lb:clipcopy", to: "#teams-key")
|
||||
|> JS.add_class(
|
||||
"tooltip top",
|
||||
to: "#teams-key-toggle [data-copy]",
|
||||
transition: {"ease-out duration-200", "opacity-0", "opacity-100"}
|
||||
)
|
||||
|> JS.remove_class(
|
||||
"tooltip top",
|
||||
to: "#teams-key-toggle [data-copy]",
|
||||
transition: {"ease-out duration-200", "opacity-0", "opacity-100"},
|
||||
time: 2000
|
||||
)
|
||||
}
|
||||
>
|
||||
<.remix_icon icon="clipboard-line" class="text-xl" />
|
||||
</button>
|
||||
|
||||
<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={@form}
|
||||
phx-submit="save"
|
||||
phx-change="validate"
|
||||
phx-target={@myself}
|
||||
>
|
||||
<div class="grid grid-cols-1 md:grid-cols-1 gap-3">
|
||||
<.emoji_field field={f[:hub_emoji]} label="Emoji" />
|
||||
<button
|
||||
class="icon-button"
|
||||
data-show
|
||||
type="button"
|
||||
aria-label="show password"
|
||||
phx-click={
|
||||
JS.remove_attribute("type", to: "#teams-key-toggle input")
|
||||
|> JS.set_attribute({"type", "text"}, to: "#teams-key-toggle input")
|
||||
|> JS.add_class("hidden", to: "#teams-key-toggle [data-show]")
|
||||
|> JS.remove_class("hidden", to: "#teams-key-toggle [data-hide]")
|
||||
}
|
||||
>
|
||||
<.remix_icon icon="eye-line" class="text-xl" />
|
||||
</button>
|
||||
<button
|
||||
class="icon-button hidden"
|
||||
data-hide
|
||||
type="button"
|
||||
aria-label="hide password"
|
||||
phx-click={
|
||||
JS.remove_attribute("type", to: "#teams-key-toggle input")
|
||||
|> JS.set_attribute({"type", "password"}, to: "#teams-key-toggle input")
|
||||
|> JS.remove_class("hidden", to: "#teams-key-toggle [data-show]")
|
||||
|> JS.add_class("hidden", to: "#teams-key-toggle [data-hide]")
|
||||
}
|
||||
>
|
||||
<.remix_icon icon="eye-off-line" class="text-xl" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</.modal>
|
||||
|
||||
<button class="button-base button-blue" type="submit" phx-disable-with="Updating...">
|
||||
Update Hub
|
||||
</button>
|
||||
</.form>
|
||||
<div class="space-y-8">
|
||||
<div class="flex relative">
|
||||
<LayoutHelpers.title text={"#{@hub.hub_emoji} #{@hub.hub_name}"} />
|
||||
|
||||
<button
|
||||
phx-click={JS.push("delete_hub", value: %{id: @hub.id})}
|
||||
class="absolute right-0 button-base button-red"
|
||||
>
|
||||
Delete hub
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<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={@form}
|
||||
phx-submit="save"
|
||||
phx-change="validate"
|
||||
phx-target={@myself}
|
||||
>
|
||||
<div class="grid grid-cols-1 md:grid-cols-1 gap-3">
|
||||
<.emoji_field field={f[:hub_emoji]} label="Emoji" />
|
||||
</div>
|
||||
|
||||
<button class="button-base button-blue" type="submit" phx-disable-with="Updating...">
|
||||
Update Hub
|
||||
</button>
|
||||
</.form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -9,32 +9,16 @@ defmodule LivebookWeb.Hub.EditLive do
|
|||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
{:ok,
|
||||
assign(socket,
|
||||
hub: nil,
|
||||
secrets: [],
|
||||
type: nil,
|
||||
page_title: "Hub - Livebook",
|
||||
env_var_id: nil,
|
||||
secret_name: nil
|
||||
)}
|
||||
{:ok, assign(socket, hub: nil, type: nil, page_title: "Hub - Livebook", params: %{})}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_params(params, _url, socket) do
|
||||
Livebook.Hubs.subscribe([:secrets])
|
||||
Hubs.subscribe([:secrets])
|
||||
hub = Hubs.fetch_hub!(params["id"])
|
||||
type = Provider.type(hub)
|
||||
|
||||
{:noreply,
|
||||
assign(socket,
|
||||
hub: hub,
|
||||
type: type,
|
||||
secrets: Hubs.get_secrets(hub),
|
||||
params: params,
|
||||
env_var_id: params["env_var_id"],
|
||||
secret_name: params["secret_name"]
|
||||
)}
|
||||
{:noreply, assign(socket, hub: hub, type: type, params: params, counter: 0)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
@ -50,8 +34,8 @@ defmodule LivebookWeb.Hub.EditLive do
|
|||
type={@type}
|
||||
hub={@hub}
|
||||
live_action={@live_action}
|
||||
secrets={@secrets}
|
||||
secret_name={@secret_name}
|
||||
params={@params}
|
||||
counter={@counter}
|
||||
/>
|
||||
</div>
|
||||
</LayoutHelpers.layout>
|
||||
|
@ -63,16 +47,23 @@ defmodule LivebookWeb.Hub.EditLive do
|
|||
<.live_component
|
||||
module={LivebookWeb.Hub.Edit.PersonalComponent}
|
||||
hub={@hub}
|
||||
secrets={@secrets}
|
||||
params={@params}
|
||||
live_action={@live_action}
|
||||
secret_name={@secret_name}
|
||||
counter={@counter}
|
||||
id="personal-form"
|
||||
/>
|
||||
"""
|
||||
end
|
||||
|
||||
defp hub_component(%{type: "team"} = assigns) do
|
||||
~H(<.live_component module={LivebookWeb.Hub.Edit.TeamComponent} hub={@hub} id="team-form" />)
|
||||
~H"""
|
||||
<.live_component
|
||||
module={LivebookWeb.Hub.Edit.TeamComponent}
|
||||
hub={@hub}
|
||||
params={@params}
|
||||
id="team-form"
|
||||
/>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
@ -98,21 +89,21 @@ defmodule LivebookWeb.Hub.EditLive do
|
|||
def handle_info({:secret_created, %{hub_id: id}}, %{assigns: %{hub: %{id: id}}} = socket) do
|
||||
{:noreply,
|
||||
socket
|
||||
|> refresh_secrets()
|
||||
|> increment_counter()
|
||||
|> put_flash(:success, "Secret created successfully")}
|
||||
end
|
||||
|
||||
def handle_info({:secret_updated, %{hub_id: id}}, %{assigns: %{hub: %{id: id}}} = socket) do
|
||||
{:noreply,
|
||||
socket
|
||||
|> refresh_secrets()
|
||||
|> increment_counter()
|
||||
|> put_flash(:success, "Secret updated successfully")}
|
||||
end
|
||||
|
||||
def handle_info({:secret_deleted, %{hub_id: id}}, %{assigns: %{hub: %{id: id}}} = socket) do
|
||||
{:noreply,
|
||||
socket
|
||||
|> refresh_secrets()
|
||||
|> increment_counter()
|
||||
|> put_flash(:success, "Secret deleted successfully")}
|
||||
end
|
||||
|
||||
|
@ -120,7 +111,5 @@ defmodule LivebookWeb.Hub.EditLive do
|
|||
{:noreply, socket}
|
||||
end
|
||||
|
||||
defp refresh_secrets(socket) do
|
||||
assign(socket, secrets: Livebook.Hubs.get_secrets(socket.assigns.hub))
|
||||
end
|
||||
defp increment_counter(socket), do: assign(socket, counter: socket.assigns.counter + 1)
|
||||
end
|
||||
|
|
|
@ -132,7 +132,7 @@ defmodule LivebookWeb.Hub.NewLive do
|
|||
</div>
|
||||
|
||||
<.password_field
|
||||
readonly={@selected_option == "new-org"}
|
||||
:if={@selected_option == "join-org"}
|
||||
field={f[:teams_key]}
|
||||
label="Livebook Teams Key"
|
||||
/>
|
||||
|
@ -281,7 +281,7 @@ defmodule LivebookWeb.Hub.NewLive do
|
|||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:success, "Hub added successfully")
|
||||
|> push_navigate(to: ~p"/hub/#{hub.id}")}
|
||||
|> push_navigate(to: ~p"/hub/#{hub.id}?show-key=true")}
|
||||
|
||||
{:error, :expired} ->
|
||||
changeset = Teams.change_org(org, %{user_code: nil})
|
||||
|
@ -293,6 +293,12 @@ defmodule LivebookWeb.Hub.NewLive do
|
|||
|> assign_form(changeset)}
|
||||
|
||||
{:transport_error, message} ->
|
||||
Process.send_after(
|
||||
self(),
|
||||
{:check_completion_data, device_code},
|
||||
@check_completion_data_interval
|
||||
)
|
||||
|
||||
{:noreply, put_flash(socket, :error, message)}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,99 +0,0 @@
|
|||
defmodule Livebook.Hubs.EnterpriseClientTest do
|
||||
use Livebook.EnterpriseIntegrationCase, async: true
|
||||
@moduletag :capture_log
|
||||
|
||||
alias Livebook.Hubs.EnterpriseClient
|
||||
alias Livebook.Secrets.Secret
|
||||
|
||||
setup do
|
||||
Livebook.Hubs.subscribe([:connection, :secrets])
|
||||
:ok
|
||||
end
|
||||
|
||||
describe "start_link/1" do
|
||||
test "successfully authenticates the web socket connection", %{url: url, token: token} do
|
||||
enterprise = build(:enterprise, url: url, token: token)
|
||||
|
||||
EnterpriseClient.start_link(enterprise)
|
||||
assert_receive :hub_connected
|
||||
end
|
||||
|
||||
test "rejects the websocket with invalid address", %{token: token} do
|
||||
enterprise = build(:enterprise, url: "http://localhost:9999", token: token)
|
||||
|
||||
EnterpriseClient.start_link(enterprise)
|
||||
assert_receive {:hub_connection_failed, "connection refused"}
|
||||
end
|
||||
|
||||
test "rejects the web socket connection with invalid credentials", %{url: url} do
|
||||
enterprise = build(:enterprise, url: url, token: "foo")
|
||||
|
||||
EnterpriseClient.start_link(enterprise)
|
||||
assert_receive {:hub_connection_failed, reason}
|
||||
assert reason =~ "the given token is invalid"
|
||||
end
|
||||
end
|
||||
|
||||
describe "handle events" do
|
||||
setup %{test: test, url: url, token: token} do
|
||||
node = EnterpriseServer.get_node()
|
||||
hub_id = "enterprise-#{test}"
|
||||
|
||||
insert_hub(:enterprise,
|
||||
id: hub_id,
|
||||
external_id: to_string(test),
|
||||
url: url,
|
||||
token: token
|
||||
)
|
||||
|
||||
assert_receive :hub_connected
|
||||
|
||||
{:ok, node: node, hub_id: hub_id}
|
||||
end
|
||||
|
||||
test "receives a secret_created event", %{node: node, hub_id: id} do
|
||||
name = "API_TOKEN_ID"
|
||||
value = Livebook.Utils.random_id()
|
||||
:erpc.call(node, Enterprise.Integration, :create_secret, [name, value])
|
||||
secret = %Secret{name: name, value: value, hub_id: id, readonly: true}
|
||||
|
||||
assert_receive {:secret_created, ^secret}
|
||||
assert secret in EnterpriseClient.get_secrets(id)
|
||||
end
|
||||
|
||||
test "receives a secret_updated event", %{node: node, hub_id: id} do
|
||||
name = "SUPER_SUDO_USER"
|
||||
value = "JakePeralta"
|
||||
new_value = "ChonkyCat"
|
||||
enterprise_secret = :erpc.call(node, Enterprise.Integration, :create_secret, [name, value])
|
||||
secret = %Secret{name: name, value: value, hub_id: id, readonly: true}
|
||||
updated_secret = %Secret{name: name, value: new_value, hub_id: id, readonly: true}
|
||||
|
||||
assert_receive {:secret_created, ^secret}
|
||||
assert secret in EnterpriseClient.get_secrets(id)
|
||||
refute updated_secret in EnterpriseClient.get_secrets(id)
|
||||
|
||||
:erpc.call(node, Enterprise.Integration, :update_secret, [enterprise_secret, new_value])
|
||||
|
||||
assert_receive {:secret_updated, ^updated_secret}
|
||||
|
||||
assert updated_secret in EnterpriseClient.get_secrets(id)
|
||||
refute secret in EnterpriseClient.get_secrets(id)
|
||||
end
|
||||
|
||||
test "receives a secret_deleted event", %{node: node, hub_id: id} do
|
||||
name = "SUPER_DELETE"
|
||||
value = "JakePeralta"
|
||||
enteprise_secret = :erpc.call(node, Enterprise.Integration, :create_secret, [name, value])
|
||||
secret = %Secret{name: name, value: value, hub_id: id, readonly: true}
|
||||
|
||||
assert_receive {:secret_created, ^secret}
|
||||
assert secret in EnterpriseClient.get_secrets(id)
|
||||
|
||||
:erpc.call(node, Enterprise.Integration, :delete_secret, [enteprise_secret])
|
||||
|
||||
assert_receive {:secret_deleted, ^secret}
|
||||
refute secret in EnterpriseClient.get_secrets(id)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,225 +0,0 @@
|
|||
defmodule Livebook.Hubs.FlyClientTest do
|
||||
use Livebook.DataCase
|
||||
|
||||
alias Livebook.Hubs.{Fly, FlyClient}
|
||||
|
||||
setup do
|
||||
bypass = Bypass.open()
|
||||
|
||||
Application.put_env(
|
||||
:livebook,
|
||||
:fly_graphql_endpoint,
|
||||
"http://localhost:#{bypass.port}"
|
||||
)
|
||||
|
||||
on_exit(fn ->
|
||||
Application.delete_env(:livebook, :fly_graphql_endpoint)
|
||||
end)
|
||||
|
||||
{:ok, bypass: bypass, url: "http://localhost:#{bypass.port}"}
|
||||
end
|
||||
|
||||
describe "fetch_apps/1" do
|
||||
test "fetches an empty list of apps", %{bypass: bypass} do
|
||||
response = %{"data" => %{"apps" => %{"nodes" => []}}}
|
||||
|
||||
Bypass.expect_once(bypass, "POST", "/", fn conn ->
|
||||
conn
|
||||
|> Plug.Conn.put_resp_content_type("application/json")
|
||||
|> Plug.Conn.resp(200, Jason.encode!(response))
|
||||
end)
|
||||
|
||||
assert {:ok, []} = FlyClient.fetch_apps("some valid token")
|
||||
end
|
||||
|
||||
test "fetches a list of apps", %{bypass: bypass} do
|
||||
app = %{
|
||||
"id" => "foo-app",
|
||||
"organization" => %{
|
||||
"id" => "l3soyvjmvtmwtl6l2drnbfuvltipprge",
|
||||
"name" => "Foo Bar",
|
||||
"type" => "PERSONAL"
|
||||
}
|
||||
}
|
||||
|
||||
app_id = app["id"]
|
||||
|
||||
response = %{"data" => %{"apps" => %{"nodes" => [app]}}}
|
||||
|
||||
Bypass.expect_once(bypass, "POST", "/", fn conn ->
|
||||
conn
|
||||
|> Plug.Conn.put_resp_content_type("application/json")
|
||||
|> Plug.Conn.resp(200, Jason.encode!(response))
|
||||
end)
|
||||
|
||||
assert {:ok, [%Fly{application_id: ^app_id}]} = FlyClient.fetch_apps("some valid token")
|
||||
end
|
||||
|
||||
test "returns unauthorized when token is invalid", %{bypass: bypass} do
|
||||
error = %{"extensions" => %{"code" => "UNAUTHORIZED"}}
|
||||
response = %{"data" => nil, "errors" => [error]}
|
||||
|
||||
Bypass.expect_once(bypass, "POST", "/", fn conn ->
|
||||
conn
|
||||
|> Plug.Conn.put_resp_content_type("application/json")
|
||||
|> Plug.Conn.resp(200, Jason.encode!(response))
|
||||
end)
|
||||
|
||||
assert {:error, "request failed with code: UNAUTHORIZED"} = FlyClient.fetch_apps("foo")
|
||||
end
|
||||
end
|
||||
|
||||
describe "fetch_app/1" do
|
||||
test "fetches an application", %{bypass: bypass} do
|
||||
app = %{
|
||||
"id" => "foo-app",
|
||||
"name" => "foo-app",
|
||||
"hostname" => "foo-app.fly.dev",
|
||||
"platformVersion" => "nomad",
|
||||
"deployed" => true,
|
||||
"status" => "running",
|
||||
"secrets" => [
|
||||
%{
|
||||
"createdAt" => to_string(DateTime.utc_now()),
|
||||
"digest" => to_string(Livebook.Utils.random_cookie()),
|
||||
"id" => Livebook.Utils.random_short_id(),
|
||||
"name" => "FOO"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
response = %{"data" => %{"app" => app}}
|
||||
|
||||
Bypass.expect_once(bypass, "POST", "/", fn conn ->
|
||||
conn
|
||||
|> Plug.Conn.put_resp_content_type("application/json")
|
||||
|> Plug.Conn.resp(200, Jason.encode!(response))
|
||||
end)
|
||||
|
||||
hub = build(:fly)
|
||||
assert {:ok, ^app} = FlyClient.fetch_app(hub)
|
||||
end
|
||||
|
||||
test "returns unauthorized when token is invalid", %{bypass: bypass} do
|
||||
error = %{"extensions" => %{"code" => "UNAUTHORIZED"}}
|
||||
response = %{"data" => nil, "errors" => [error]}
|
||||
|
||||
Bypass.expect_once(bypass, "POST", "/", fn conn ->
|
||||
conn
|
||||
|> Plug.Conn.put_resp_content_type("application/json")
|
||||
|> Plug.Conn.resp(200, Jason.encode!(response))
|
||||
end)
|
||||
|
||||
hub = build(:fly)
|
||||
assert {:error, "request failed with code: UNAUTHORIZED"} = FlyClient.fetch_app(hub)
|
||||
end
|
||||
end
|
||||
|
||||
describe "set_secrets/2" do
|
||||
test "puts a list of secrets inside application", %{bypass: bypass} do
|
||||
secrets = [
|
||||
%{
|
||||
"createdAt" => to_string(DateTime.utc_now()),
|
||||
"digest" => to_string(Livebook.Utils.random_cookie()),
|
||||
"id" => Livebook.Utils.random_short_id(),
|
||||
"name" => "FOO"
|
||||
}
|
||||
]
|
||||
|
||||
response = %{"data" => %{"setSecrets" => %{"app" => %{"secrets" => secrets}}}}
|
||||
|
||||
Bypass.expect_once(bypass, "POST", "/", fn conn ->
|
||||
conn
|
||||
|> Plug.Conn.put_resp_content_type("application/json")
|
||||
|> Plug.Conn.resp(200, Jason.encode!(response))
|
||||
end)
|
||||
|
||||
hub = build(:fly)
|
||||
assert {:ok, ^secrets} = FlyClient.set_secrets(hub, [%{key: "FOO", value: "BAR"}])
|
||||
end
|
||||
|
||||
test "returns error when input is invalid", %{bypass: bypass} do
|
||||
message =
|
||||
"Variable $input of type SetSecretsInput! was provided invalid value for secrets.0.Value (Field is not defined on SecretInput), secrets.0.value (Expected value to not be null)"
|
||||
|
||||
error = %{
|
||||
"extensions" => %{
|
||||
"problems" => [
|
||||
%{
|
||||
"explanation" => "Field is not defined on SecretInput",
|
||||
"path" => ["secrets", 0, "Value"]
|
||||
},
|
||||
%{
|
||||
"explanation" => "Expected value to not be null",
|
||||
"path" => ["secrets", 0, "value"]
|
||||
}
|
||||
],
|
||||
"value" => %{
|
||||
"appId" => "myfoo-test-livebook",
|
||||
"secrets" => [%{"Value" => "BAR", "key" => "FOO"}]
|
||||
}
|
||||
},
|
||||
"locations" => [%{"column" => 10, "line" => 1}],
|
||||
"message" => message
|
||||
}
|
||||
|
||||
response = %{"data" => nil, "errors" => [error]}
|
||||
|
||||
Bypass.expect_once(bypass, "POST", "/", fn conn ->
|
||||
conn
|
||||
|> Plug.Conn.put_resp_content_type("application/json")
|
||||
|> Plug.Conn.resp(200, Jason.encode!(response))
|
||||
end)
|
||||
|
||||
hub = build(:fly)
|
||||
assert {:error, ^message} = FlyClient.set_secrets(hub, [%{key: "FOO", Value: "BAR"}])
|
||||
end
|
||||
|
||||
test "returns unauthorized when token is invalid", %{bypass: bypass} do
|
||||
error = %{"extensions" => %{"code" => "UNAUTHORIZED"}}
|
||||
response = %{"data" => nil, "errors" => [error]}
|
||||
|
||||
Bypass.expect_once(bypass, "POST", "/", fn conn ->
|
||||
conn
|
||||
|> Plug.Conn.put_resp_content_type("application/json")
|
||||
|> Plug.Conn.resp(200, Jason.encode!(response))
|
||||
end)
|
||||
|
||||
hub = build(:fly)
|
||||
|
||||
assert {:error, "request failed with code: UNAUTHORIZED"} =
|
||||
FlyClient.set_secrets(hub, [%{key: "FOO", value: "BAR"}])
|
||||
end
|
||||
end
|
||||
|
||||
describe "unset_secrets/2" do
|
||||
test "deletes a list of secrets inside application", %{bypass: bypass} do
|
||||
response = %{"data" => %{"unsetSecrets" => %{"app" => %{"secrets" => []}}}}
|
||||
|
||||
Bypass.expect_once(bypass, "POST", "/", fn conn ->
|
||||
conn
|
||||
|> Plug.Conn.put_resp_content_type("application/json")
|
||||
|> Plug.Conn.resp(200, Jason.encode!(response))
|
||||
end)
|
||||
|
||||
hub = build(:fly)
|
||||
assert {:ok, []} = FlyClient.unset_secrets(hub, ["FOO"])
|
||||
end
|
||||
|
||||
test "returns unauthorized when token is invalid", %{bypass: bypass} do
|
||||
error = %{"extensions" => %{"code" => "UNAUTHORIZED"}}
|
||||
response = %{"data" => nil, "errors" => [error]}
|
||||
|
||||
Bypass.expect_once(bypass, "POST", "/", fn conn ->
|
||||
conn
|
||||
|> Plug.Conn.put_resp_content_type("application/json")
|
||||
|> Plug.Conn.resp(200, Jason.encode!(response))
|
||||
end)
|
||||
|
||||
hub = build(:fly)
|
||||
|
||||
assert {:error, "request failed with code: UNAUTHORIZED"} =
|
||||
FlyClient.unset_secrets(hub, ["FOO"])
|
||||
end
|
||||
end
|
||||
end
|
|
@ -4,53 +4,53 @@ defmodule Livebook.HubsTest do
|
|||
alias Livebook.Hubs
|
||||
|
||||
test "get_hubs/0 returns a list of persisted hubs" do
|
||||
fly = insert_hub(:fly, id: "fly-baz")
|
||||
assert fly in Hubs.get_hubs()
|
||||
team = insert_hub(:team, id: "team-baz")
|
||||
assert team in Hubs.get_hubs()
|
||||
|
||||
Hubs.delete_hub("fly-baz")
|
||||
refute fly in Hubs.get_hubs()
|
||||
Hubs.delete_hub("team-baz")
|
||||
refute team in Hubs.get_hubs()
|
||||
end
|
||||
|
||||
test "get_metadata/0 returns a list of persisted hubs normalized" do
|
||||
fly = insert_hub(:fly, id: "fly-livebook")
|
||||
metadata = Hubs.Provider.to_metadata(fly)
|
||||
team = insert_hub(:team, id: "team-livebook")
|
||||
metadata = Hubs.Provider.to_metadata(team)
|
||||
|
||||
assert metadata in Hubs.get_metadatas()
|
||||
|
||||
Hubs.delete_hub("fly-livebook")
|
||||
Hubs.delete_hub("team-livebook")
|
||||
refute metadata in Hubs.get_metadatas()
|
||||
end
|
||||
|
||||
test "fetch_hub!/1 returns one persisted fly" do
|
||||
test "fetch_hub!/1 returns one persisted team" do
|
||||
assert_raise Livebook.Storage.NotFoundError,
|
||||
~s/could not find entry in \"hubs\" with ID "fly-exception-foo"/,
|
||||
~s/could not find entry in \"hubs\" with ID "team-exception-foo"/,
|
||||
fn ->
|
||||
Hubs.fetch_hub!("fly-exception-foo")
|
||||
Hubs.fetch_hub!("team-exception-foo")
|
||||
end
|
||||
|
||||
fly = insert_hub(:fly, id: "fly-exception-foo")
|
||||
team = insert_hub(:team, id: "team-exception-foo")
|
||||
|
||||
assert Hubs.fetch_hub!("fly-exception-foo") == fly
|
||||
assert Hubs.fetch_hub!("team-exception-foo") == team
|
||||
end
|
||||
|
||||
test "hub_exists?/1" do
|
||||
refute Hubs.hub_exists?("fly-bar")
|
||||
insert_hub(:fly, id: "fly-bar")
|
||||
assert Hubs.hub_exists?("fly-bar")
|
||||
refute Hubs.hub_exists?("team-bar")
|
||||
insert_hub(:team, id: "team-bar")
|
||||
assert Hubs.hub_exists?("team-bar")
|
||||
end
|
||||
|
||||
test "save_hub/1 persists hub" do
|
||||
fly = build(:fly, id: "fly-foo")
|
||||
Hubs.save_hub(fly)
|
||||
team = build(:team, id: "team-foo")
|
||||
Hubs.save_hub(team)
|
||||
|
||||
assert Hubs.fetch_hub!("fly-foo") == fly
|
||||
assert Hubs.fetch_hub!("team-foo") == team
|
||||
end
|
||||
|
||||
test "save_hub/1 updates hub" do
|
||||
fly = insert_hub(:fly, id: "fly-foo2")
|
||||
Hubs.save_hub(%{fly | hub_emoji: "🐈"})
|
||||
team = insert_hub(:team, id: "team-foo2")
|
||||
Hubs.save_hub(%{team | hub_emoji: "🐈"})
|
||||
|
||||
refute Hubs.fetch_hub!("fly-foo2") == fly
|
||||
assert Hubs.fetch_hub!("fly-foo2").hub_emoji == "🐈"
|
||||
refute Hubs.fetch_hub!("team-foo2") == team
|
||||
assert Hubs.fetch_hub!("team-foo2").hub_emoji == "🐈"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1122,16 +1122,16 @@ defmodule Livebook.LiveMarkdown.ExportTest do
|
|||
end
|
||||
|
||||
test "persists hub id when not default" do
|
||||
Livebook.Factory.insert_hub(:fly, id: "fly-persisted-id")
|
||||
Livebook.Factory.insert_hub(:team, id: "team-persisted-id")
|
||||
|
||||
notebook = %{
|
||||
Notebook.new()
|
||||
| name: "My Notebook",
|
||||
hub_id: "fly-persisted-id"
|
||||
hub_id: "team-persisted-id"
|
||||
}
|
||||
|
||||
expected_document = """
|
||||
<!-- livebook:{"hub_id":"fly-persisted-id"} -->
|
||||
<!-- livebook:{"hub_id":"team-persisted-id"} -->
|
||||
|
||||
# My Notebook
|
||||
"""
|
||||
|
|
|
@ -721,17 +721,17 @@ defmodule Livebook.LiveMarkdown.ImportTest do
|
|||
end
|
||||
|
||||
test "imports notebook hub id when exists" do
|
||||
Livebook.Factory.insert_hub(:fly, id: "fly-persisted-id")
|
||||
Livebook.Factory.insert_hub(:team, id: "team-persisted-id")
|
||||
|
||||
markdown = """
|
||||
<!-- livebook:{"hub_id":"fly-persisted-id"} -->
|
||||
<!-- livebook:{"hub_id":"team-persisted-id"} -->
|
||||
|
||||
# My Notebook
|
||||
"""
|
||||
|
||||
{notebook, []} = Import.notebook_from_livemd(markdown)
|
||||
|
||||
assert %Notebook{name: "My Notebook", hub_id: "fly-persisted-id"} = notebook
|
||||
assert %Notebook{name: "My Notebook", hub_id: "team-persisted-id"} = notebook
|
||||
end
|
||||
|
||||
test "imports ignores hub id when does not exist" do
|
||||
|
|
|
@ -78,12 +78,14 @@ defmodule Livebook.TeamsTest do
|
|||
)
|
||||
|
||||
%{
|
||||
org: %{id: id, name: name, keys: [%{id: org_key_id}]},
|
||||
user: %{id: user_id},
|
||||
sessions: [%{token: token}]
|
||||
} = org_request.user_org
|
||||
token: token,
|
||||
user_org: %{
|
||||
org: %{id: id, name: name, keys: [%{id: org_key_id}]},
|
||||
user: %{id: user_id}
|
||||
}
|
||||
} = org_request.user_org_session
|
||||
|
||||
assert Teams.get_org_request_completion_data(org) ==
|
||||
assert Teams.get_org_request_completion_data(org, org_request.device_code) ==
|
||||
{:ok,
|
||||
%{
|
||||
"id" => id,
|
||||
|
@ -108,12 +110,13 @@ defmodule Livebook.TeamsTest do
|
|||
user_code: org_request.user_code
|
||||
)
|
||||
|
||||
assert Teams.get_org_request_completion_data(org) == {:ok, :awaiting_confirmation}
|
||||
assert Teams.get_org_request_completion_data(org, org_request.device_code) ==
|
||||
{:ok, :awaiting_confirmation}
|
||||
end
|
||||
|
||||
test "returns error when org request doesn't exist" do
|
||||
org = build(:org, id: 0)
|
||||
assert {:transport_error, _embarrassing} = Teams.get_org_request_completion_data(org)
|
||||
assert {:transport_error, _embarrassing} = Teams.get_org_request_completion_data(org, "")
|
||||
end
|
||||
|
||||
test "returns error when org request expired", %{node: node} do
|
||||
|
@ -135,7 +138,8 @@ defmodule Livebook.TeamsTest do
|
|||
user_code: org_request.user_code
|
||||
)
|
||||
|
||||
assert Teams.get_org_request_completion_data(org) == {:error, :expired}
|
||||
assert Teams.get_org_request_completion_data(org, org_request.device_code) ==
|
||||
{:error, :expired}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,168 +0,0 @@
|
|||
defmodule Livebook.WebSocket.ClientConnectionTest do
|
||||
use Livebook.EnterpriseIntegrationCase, async: true
|
||||
|
||||
@moduletag :capture_log
|
||||
|
||||
alias Livebook.WebSocket.ClientConnection
|
||||
|
||||
describe "connect" do
|
||||
test "successfully authenticates the websocket connection", %{url: url, token: token} do
|
||||
headers = [{"X-Auth-Token", token}]
|
||||
|
||||
assert {:ok, _conn} = ClientConnection.start_link(self(), url, headers)
|
||||
assert_receive {:connect, :ok, :connected}
|
||||
end
|
||||
|
||||
test "rejects the websocket with invalid address", %{token: token} do
|
||||
headers = [{"X-Auth-Token", token}]
|
||||
|
||||
assert {:ok, _conn} = ClientConnection.start_link(self(), "http://localhost:9999", headers)
|
||||
assert_receive {:connect, :error, "connection refused"}
|
||||
end
|
||||
|
||||
test "rejects the websocket connection with invalid credentials", %{url: url} do
|
||||
headers = [{"X-Auth-Token", "foo"}]
|
||||
|
||||
assert {:ok, _conn} = ClientConnection.start_link(self(), url, headers)
|
||||
|
||||
assert_receive {:connect, :error, reason}
|
||||
assert reason =~ "the given token is invalid"
|
||||
|
||||
assert {:ok, _conn} = ClientConnection.start_link(self(), url)
|
||||
|
||||
assert_receive {:connect, :error, reason}
|
||||
assert reason =~ "could not get the token from the connection"
|
||||
end
|
||||
end
|
||||
|
||||
describe "send_request/2" do
|
||||
setup %{url: url, token: token} do
|
||||
headers = [{"X-Auth-Token", token}]
|
||||
|
||||
{:ok, conn} = ClientConnection.start_link(self(), url, headers)
|
||||
assert_receive {:connect, :ok, :connected}
|
||||
|
||||
{:ok, conn: conn}
|
||||
end
|
||||
|
||||
test "successfully sends a session request", %{conn: conn, user: %{id: id, email: email}} do
|
||||
data = LivebookProto.build_handshake_request(app_version: Livebook.Config.app_version())
|
||||
|
||||
assert {:handshake, handshake_response} = ClientConnection.send_request(conn, data)
|
||||
assert %{id: _, user: %{id: ^id, email: ^email}} = handshake_response
|
||||
end
|
||||
|
||||
test "successfully sends a create secret message", %{conn: conn} do
|
||||
data = LivebookProto.build_create_secret_request(name: "MY_USERNAME", value: "Jake Peralta")
|
||||
assert {:create_secret, _} = ClientConnection.send_request(conn, data)
|
||||
end
|
||||
|
||||
test "sends a create secret message, but receive a changeset error", %{conn: conn} do
|
||||
data = LivebookProto.build_create_secret_request(name: "MY_USERNAME", value: "")
|
||||
assert {:changeset_error, errors} = ClientConnection.send_request(conn, data)
|
||||
assert "can't be blank" in errors.value
|
||||
end
|
||||
end
|
||||
|
||||
describe "reconnect event" do
|
||||
setup %{test: name} do
|
||||
start_new_instance(name)
|
||||
|
||||
url = EnterpriseServer.url(name)
|
||||
token = EnterpriseServer.token(name)
|
||||
headers = [{"X-Auth-Token", token}]
|
||||
|
||||
assert {:ok, conn} = ClientConnection.start_link(self(), url, headers)
|
||||
assert_receive {:connect, :ok, :connected}
|
||||
|
||||
on_exit(fn -> stop_new_instance(name) end)
|
||||
|
||||
{:ok, conn: conn}
|
||||
end
|
||||
|
||||
test "receives the disconnect message from websocket server", %{conn: conn, test: name} do
|
||||
EnterpriseServer.disconnect(name)
|
||||
|
||||
assert_receive {:connect, :error, "socket closed"}
|
||||
assert_receive {:connect, :error, "connection refused"}
|
||||
|
||||
assert Process.alive?(conn)
|
||||
end
|
||||
|
||||
test "reconnects after websocket server is up", %{test: name} do
|
||||
EnterpriseServer.disconnect(name)
|
||||
|
||||
assert_receive {:connect, :error, "socket closed"}
|
||||
assert_receive {:connect, :error, "connection refused"}
|
||||
|
||||
Process.sleep(1000)
|
||||
|
||||
# Wait until the server is up again
|
||||
assert EnterpriseServer.reconnect(name) == :ok
|
||||
|
||||
assert_receive {:connect, :ok, :connected}, 5000
|
||||
end
|
||||
end
|
||||
|
||||
describe "handle events from server" do
|
||||
setup %{url: url, token: token} do
|
||||
headers = [{"X-Auth-Token", token}]
|
||||
|
||||
{:ok, conn} = ClientConnection.start_link(self(), url, headers)
|
||||
assert_receive {:connect, :ok, :connected}
|
||||
|
||||
{:ok, conn: conn}
|
||||
end
|
||||
|
||||
test "receives a secret_created event", %{node: node} do
|
||||
name = "MY_SECRET_ID"
|
||||
value = Livebook.Utils.random_id()
|
||||
:erpc.call(node, Enterprise.Integration, :create_secret, [name, value])
|
||||
|
||||
assert_receive {:event, :secret_created, %{name: ^name, value: ^value}}
|
||||
end
|
||||
|
||||
test "receives a secret_updated event", %{node: node} do
|
||||
name = "API_USERNAME"
|
||||
value = "JakePeralta"
|
||||
secret = :erpc.call(node, Enterprise.Integration, :create_secret, [name, value])
|
||||
|
||||
assert_receive {:event, :secret_created, %{name: ^name, value: ^value}}
|
||||
|
||||
new_value = "ChonkyCat"
|
||||
:erpc.call(node, Enterprise.Integration, :update_secret, [secret, new_value])
|
||||
|
||||
assert_receive {:event, :secret_updated, %{name: ^name, value: ^new_value}}
|
||||
end
|
||||
|
||||
test "receives a secret_deleted event", %{node: node} do
|
||||
name = "DELETE_ME"
|
||||
value = "JakePeralta"
|
||||
secret = :erpc.call(node, Enterprise.Integration, :create_secret, [name, value])
|
||||
|
||||
assert_receive {:event, :secret_created, %{name: ^name, value: ^value}}
|
||||
|
||||
:erpc.call(node, Enterprise.Integration, :delete_secret, [secret])
|
||||
|
||||
assert_receive {:event, :secret_deleted, %{name: ^name, value: ^value}}
|
||||
end
|
||||
|
||||
test "receives a user_synchronized event", %{conn: conn, node: node} do
|
||||
data = LivebookProto.build_handshake_request(app_version: Livebook.Config.app_version())
|
||||
assert {:handshake, _} = ClientConnection.send_request(conn, data)
|
||||
|
||||
id = :erpc.call(node, Enterprise.Integration, :fetch_env!, ["ENTERPRISE_ID"])
|
||||
name = :erpc.call(node, Enterprise.Integration, :fetch_env!, ["ENTERPRISE_NAME"])
|
||||
|
||||
assert_receive {:event, :user_synchronized, %{id: ^id, name: ^name, secrets: []}}
|
||||
|
||||
secret = :erpc.call(node, Enterprise.Integration, :create_secret, ["SESSION", "123"])
|
||||
assert_receive {:event, :secret_created, %{name: "SESSION", value: "123"}}
|
||||
|
||||
assert {:handshake, _} = ClientConnection.send_request(conn, data)
|
||||
|
||||
assert_receive {:event, :user_synchronized, %{id: ^id, name: ^name, secrets: secrets}}
|
||||
assert LivebookProto.Secret.new!(name: secret.name, value: secret.value) in secrets
|
||||
end
|
||||
end
|
||||
end
|
|
@ -206,13 +206,13 @@ defmodule LivebookWeb.HomeLiveTest do
|
|||
end
|
||||
|
||||
test "render persisted hubs", %{conn: conn} do
|
||||
fly = insert_hub(:fly, id: "fly-foo-bar-id")
|
||||
team = insert_hub(:team, id: "team-foo-bar-id")
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/")
|
||||
assert html =~ "HUBS"
|
||||
assert html =~ fly.hub_name
|
||||
assert html =~ team.hub_name
|
||||
|
||||
Livebook.Hubs.delete_hub("fly-foo-bar-id")
|
||||
Livebook.Hubs.delete_hub("team-foo-bar-id")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -71,15 +71,17 @@ defmodule LivebookWeb.Hub.EditLiveTest do
|
|||
|> render_submit(attrs)
|
||||
|
||||
assert_receive {:secret_created, ^secret}
|
||||
assert_patch(view, "/hub/#{hub.id}")
|
||||
assert render(view) =~ "Secret created successfully"
|
||||
assert render(view) =~ secret.name
|
||||
assert render(element(view, "#hub-secrets-list")) =~ secret.name
|
||||
assert secret in Livebook.Hubs.get_secrets(hub)
|
||||
end
|
||||
|
||||
test "updates secret", %{conn: conn, hub: hub} do
|
||||
{:ok, view, _html} = live(conn, ~p"/hub/#{hub.id}")
|
||||
secret = insert_secret(name: "PERSONAL_EDIT_SECRET", value: "GetTheBonk")
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/hub/#{hub.id}")
|
||||
|
||||
attrs = %{
|
||||
secret: %{
|
||||
name: secret.name,
|
||||
|
@ -112,15 +114,17 @@ defmodule LivebookWeb.Hub.EditLiveTest do
|
|||
updated_secret = %{secret | value: new_value}
|
||||
|
||||
assert_receive {:secret_updated, ^updated_secret}
|
||||
assert_patch(view, "/hub/#{hub.id}")
|
||||
assert render(view) =~ "Secret updated successfully"
|
||||
assert render(view) =~ secret.name
|
||||
assert render(element(view, "#hub-secrets-list")) =~ secret.name
|
||||
assert updated_secret in Livebook.Hubs.get_secrets(hub)
|
||||
end
|
||||
|
||||
test "deletes secret", %{conn: conn, hub: hub} do
|
||||
{:ok, view, _html} = live(conn, ~p"/hub/#{hub.id}")
|
||||
secret = insert_secret(name: "PERSONAL_DELETE_SECRET", value: "GetTheBonk")
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/hub/#{hub.id}")
|
||||
|
||||
refute view
|
||||
|> element("#secrets-form button[disabled]")
|
||||
|> has_element?()
|
||||
|
|
|
@ -1,168 +0,0 @@
|
|||
defmodule LivebookWeb.Hub.New.EnterpriseComponentTest do
|
||||
use Livebook.EnterpriseIntegrationCase, async: true
|
||||
@moduletag :capture_log
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Livebook.Hubs
|
||||
|
||||
describe "enterprise" do
|
||||
test "persists new hub", %{conn: conn, url: url, token: token} do
|
||||
node = EnterpriseServer.get_node()
|
||||
id = :erpc.call(node, Enterprise.Integration, :fetch_env!, ["ENTERPRISE_ID"])
|
||||
Livebook.Hubs.delete_hub("enterprise-#{id}")
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/hub")
|
||||
|
||||
assert view
|
||||
|> element("#enterprise")
|
||||
|> render_click() =~ "2. Configure your Hub"
|
||||
|
||||
view
|
||||
|> element("#enterprise-form")
|
||||
|> render_change(%{
|
||||
"enterprise" => %{
|
||||
"url" => url,
|
||||
"token" => token
|
||||
}
|
||||
})
|
||||
|
||||
view
|
||||
|> element("#connect")
|
||||
|> render_click()
|
||||
|
||||
assert render(view) =~ to_string(id)
|
||||
|
||||
attrs = %{
|
||||
"url" => url,
|
||||
"token" => token,
|
||||
"hub_name" => "Enterprise",
|
||||
"hub_emoji" => "🐈"
|
||||
}
|
||||
|
||||
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 =~ "🐈"
|
||||
assert hubs_html =~ "/hub/enterprise-#{id}"
|
||||
assert hubs_html =~ "Enterprise"
|
||||
end
|
||||
|
||||
test "fails with invalid token", %{test: name, conn: conn} do
|
||||
start_new_instance(name)
|
||||
|
||||
url = EnterpriseServer.url(name)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/hub")
|
||||
token = "foo bar baz"
|
||||
|
||||
assert view
|
||||
|> element("#enterprise")
|
||||
|> render_click() =~ "2. Configure your Hub"
|
||||
|
||||
view
|
||||
|> element("#enterprise-form")
|
||||
|> render_change(%{
|
||||
"enterprise" => %{
|
||||
"url" => url,
|
||||
"token" => token
|
||||
}
|
||||
})
|
||||
|
||||
view
|
||||
|> element("#connect")
|
||||
|> render_click()
|
||||
|
||||
assert render(view) =~ "the given token is invalid"
|
||||
refute render(view) =~ "enterprise[hub_name]"
|
||||
after
|
||||
stop_new_instance(name)
|
||||
end
|
||||
|
||||
test "fails to create existing hub", %{test: name, conn: conn} do
|
||||
start_new_instance(name)
|
||||
|
||||
node = EnterpriseServer.get_node(name)
|
||||
url = EnterpriseServer.url(name)
|
||||
token = EnterpriseServer.token(name)
|
||||
|
||||
id = :erpc.call(node, Enterprise.Integration, :fetch_env!, ["ENTERPRISE_ID"])
|
||||
user = :erpc.call(node, Enterprise.Integration, :create_user, [])
|
||||
|
||||
another_token =
|
||||
:erpc.call(node, Enterprise.Integration, :generate_user_session_token!, [user])
|
||||
|
||||
hub =
|
||||
insert_hub(:enterprise,
|
||||
id: "enterprise-#{id}",
|
||||
external_id: id,
|
||||
url: url,
|
||||
token: another_token
|
||||
)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/hub")
|
||||
|
||||
assert view
|
||||
|> element("#enterprise")
|
||||
|> render_click() =~ "2. Configure your Hub"
|
||||
|
||||
view
|
||||
|> element("#enterprise-form")
|
||||
|> render_change(%{
|
||||
"enterprise" => %{
|
||||
"url" => url,
|
||||
"token" => token
|
||||
}
|
||||
})
|
||||
|
||||
view
|
||||
|> element("#connect")
|
||||
|> render_click()
|
||||
|
||||
assert render(view) =~ to_string(id)
|
||||
|
||||
attrs = %{
|
||||
"url" => url,
|
||||
"token" => token,
|
||||
"hub_name" => "Enterprise",
|
||||
"hub_emoji" => "🐈"
|
||||
}
|
||||
|
||||
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 =~ hub.hub_emoji
|
||||
assert hubs_html =~ ~p"/hub/#{hub.id}"
|
||||
assert hubs_html =~ hub.hub_name
|
||||
|
||||
assert Hubs.fetch_hub!(hub.id) == hub
|
||||
after
|
||||
stop_new_instance(name)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -4,7 +4,6 @@ defmodule LivebookWeb.Hub.NewLiveTest do
|
|||
alias Livebook.Teams.Org
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
@check_completion_data_interval 5000
|
||||
|
||||
test "render hub selection cards", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/hub")
|
||||
|
@ -17,9 +16,6 @@ defmodule LivebookWeb.Hub.NewLiveTest do
|
|||
describe "new-org" do
|
||||
test "persist a new hub", %{conn: conn, node: node, user: user} do
|
||||
name = "new-org-test"
|
||||
teams_key = Livebook.Teams.Org.teams_key()
|
||||
key_hash = Org.key_hash(build(:org, teams_key: teams_key))
|
||||
path = ~p"/hub/team-#{name}"
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/hub")
|
||||
|
||||
|
@ -29,7 +25,7 @@ defmodule LivebookWeb.Hub.NewLiveTest do
|
|||
|> render_click()
|
||||
|
||||
# builds the form data
|
||||
attrs = %{"org" => %{"name" => name, "teams_key" => teams_key, "emoji" => "🐈"}}
|
||||
attrs = %{"org" => %{"name" => name, "emoji" => "🐈"}}
|
||||
|
||||
# finds the form and change data
|
||||
form = element(view, "#new-org-form")
|
||||
|
@ -38,11 +34,8 @@ defmodule LivebookWeb.Hub.NewLiveTest do
|
|||
# submits the form
|
||||
render_submit(form, attrs)
|
||||
|
||||
# gets the org request by name and key hash
|
||||
org_request =
|
||||
:erpc.call(node, Hub.Integration, :get_org_request_by!, [
|
||||
[name: name, key_hash: key_hash]
|
||||
])
|
||||
# gets the org request by name
|
||||
org_request = :erpc.call(node, Hub.Integration, :get_org_request_by!, [[name: name]])
|
||||
|
||||
# check if the form has the url to confirm
|
||||
link_element = element(view, "#new-org-form a")
|
||||
|
@ -55,14 +48,18 @@ defmodule LivebookWeb.Hub.NewLiveTest do
|
|||
# check if the page redirected to edit hub page
|
||||
# and check the flash message
|
||||
%{"success" => "Hub added successfully"} =
|
||||
assert_redirect(view, path, @check_completion_data_interval)
|
||||
assert_redirect(view, "/hub/team-#{name}?show-key=true", check_completion_data_interval())
|
||||
|
||||
# access the page and shows the teams key modal
|
||||
{:ok, view, _html} = live(conn, "/hub/team-#{name}?show-key=true")
|
||||
assert has_element?(view, "#show-key-modal")
|
||||
|
||||
# access the page when closes the modal
|
||||
assert {:ok, view, _html} = live(conn, "/hub/team-#{name}")
|
||||
refute has_element?(view, "#show-key-modal")
|
||||
|
||||
# checks if the hub is in the sidebar
|
||||
{:ok, view, _html} = live(conn, path)
|
||||
hubs_html = view |> element("#hubs") |> render()
|
||||
assert hubs_html =~ "🐈"
|
||||
assert hubs_html =~ path
|
||||
assert hubs_html =~ name
|
||||
assert_hub(view, "/hub/team-#{name}", name)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -71,7 +68,6 @@ defmodule LivebookWeb.Hub.NewLiveTest do
|
|||
name = "join-org-test"
|
||||
teams_key = Livebook.Teams.Org.teams_key()
|
||||
key_hash = Org.key_hash(build(:org, teams_key: teams_key))
|
||||
path = ~p"/hub/team-#{name}"
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/hub")
|
||||
|
||||
|
@ -112,14 +108,28 @@ defmodule LivebookWeb.Hub.NewLiveTest do
|
|||
# check if the page redirected to edit hub page
|
||||
# and check the flash message
|
||||
%{"success" => "Hub added successfully"} =
|
||||
assert_redirect(view, path, @check_completion_data_interval)
|
||||
assert_redirect(view, "/hub/team-#{name}?show-key=true", check_completion_data_interval())
|
||||
|
||||
# access the page and shows the teams key modal
|
||||
{:ok, view, _html} = live(conn, "/hub/team-#{name}?show-key=true")
|
||||
assert has_element?(view, "#show-key-modal")
|
||||
|
||||
# access the page when closes the modal
|
||||
assert {:ok, view, _html} = live(conn, "/hub/team-#{name}")
|
||||
refute has_element?(view, "#show-key-modal")
|
||||
|
||||
# checks if the hub is in the sidebar
|
||||
{:ok, view, _html} = live(conn, path)
|
||||
hubs_html = view |> element("#hubs") |> render()
|
||||
assert hubs_html =~ "🐈"
|
||||
assert hubs_html =~ path
|
||||
assert hubs_html =~ name
|
||||
assert_hub(view, "/hub/team-#{name}", name)
|
||||
end
|
||||
end
|
||||
|
||||
defp check_completion_data_interval(), do: 2000
|
||||
|
||||
defp assert_hub(view, path, name, emoji \\ "🐈") do
|
||||
hubs_html = view |> element("#hubs") |> render()
|
||||
|
||||
assert hubs_html =~ emoji
|
||||
assert hubs_html =~ path
|
||||
assert hubs_html =~ name
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,200 +0,0 @@
|
|||
defmodule LivebookWeb.SessionLive.SecretsComponentTest do
|
||||
use Livebook.EnterpriseIntegrationCase, async: true
|
||||
|
||||
import Livebook.SessionHelpers
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Livebook.Session
|
||||
alias Livebook.Sessions
|
||||
|
||||
describe "enterprise" do
|
||||
setup %{test: name} do
|
||||
start_new_instance(name)
|
||||
|
||||
node = EnterpriseServer.get_node(name)
|
||||
url = EnterpriseServer.url(name)
|
||||
token = EnterpriseServer.token(name)
|
||||
|
||||
id = :erpc.call(node, Enterprise.Integration, :fetch_env!, ["ENTERPRISE_ID"])
|
||||
hub_id = "enterprise-#{id}"
|
||||
|
||||
Livebook.Hubs.subscribe([:connection, :secrets])
|
||||
Livebook.Hubs.delete_hub(hub_id)
|
||||
|
||||
enterprise =
|
||||
insert_hub(:enterprise,
|
||||
id: hub_id,
|
||||
external_id: id,
|
||||
url: url,
|
||||
token: token
|
||||
)
|
||||
|
||||
{:ok, session} = Sessions.create_session(notebook: Livebook.Notebook.new())
|
||||
Session.set_notebook_hub(session.pid, hub_id)
|
||||
|
||||
on_exit(fn ->
|
||||
Livebook.Hubs.delete_hub(hub_id)
|
||||
Session.close(session.pid)
|
||||
stop_new_instance(name)
|
||||
end)
|
||||
|
||||
{:ok, enterprise: enterprise, session: session, node: node}
|
||||
end
|
||||
|
||||
test "creates a secret on Enterprise hub",
|
||||
%{conn: conn, session: session, enterprise: enterprise} do
|
||||
id = enterprise.id
|
||||
|
||||
secret =
|
||||
build(:secret, name: "BIG_IMPORTANT_SECRET", value: "123", hub_id: id, readonly: true)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/sessions/#{session.id}/secrets")
|
||||
|
||||
attrs = %{
|
||||
secret: %{
|
||||
name: secret.name,
|
||||
value: secret.value,
|
||||
hub_id: enterprise.id
|
||||
}
|
||||
}
|
||||
|
||||
form = element(view, ~s{form[phx-submit="save"]})
|
||||
render_change(form, attrs)
|
||||
render_submit(form, attrs)
|
||||
|
||||
assert_receive {:secret_created, ^secret}
|
||||
assert has_element?(view, "#hub-#{enterprise.id}-secret-#{secret.name}")
|
||||
end
|
||||
|
||||
test "toggle a secret from Enterprise hub",
|
||||
%{conn: conn, session: session, enterprise: enterprise, node: node} do
|
||||
secret =
|
||||
build(:secret,
|
||||
name: "POSTGRES_PASSWORD",
|
||||
value: "postgres",
|
||||
hub_id: enterprise.id,
|
||||
readonly: true
|
||||
)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/sessions/#{session.id}")
|
||||
|
||||
:erpc.call(node, Enterprise.Integration, :create_secret, [secret.name, secret.value])
|
||||
assert_receive {:secret_created, ^secret}
|
||||
|
||||
Session.set_secret(session.pid, secret)
|
||||
assert_session_secret(view, session.pid, secret)
|
||||
end
|
||||
|
||||
test "adding a missing secret using 'Add secret' button",
|
||||
%{conn: conn, session: session, enterprise: enterprise} do
|
||||
secret =
|
||||
build(:secret,
|
||||
name: "PGPASS",
|
||||
value: "postgres",
|
||||
hub_id: enterprise.id,
|
||||
readonly: true
|
||||
)
|
||||
|
||||
# Subscribe and executes the code to trigger
|
||||
# the `System.EnvError` exception and outputs the 'Add secret' button
|
||||
Session.subscribe(session.id)
|
||||
section_id = insert_section(session.pid)
|
||||
code = ~s{System.fetch_env!("LB_#{secret.name}")}
|
||||
cell_id = insert_text_cell(session.pid, section_id, :code, code)
|
||||
|
||||
Session.queue_cell_evaluation(session.pid, cell_id)
|
||||
assert_receive {:operation, {:add_cell_evaluation_response, _, ^cell_id, _, _}}
|
||||
|
||||
# Enters the session to check if the button exists
|
||||
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")
|
||||
expected_url = ~p"/sessions/#{session.id}/secrets?secret_name=#{secret.name}"
|
||||
add_secret_button = element(view, "a[href='#{expected_url}']")
|
||||
assert has_element?(add_secret_button)
|
||||
|
||||
# Clicks the button and fills the form to create a new secret
|
||||
# that prefilled the name with the received from exception.
|
||||
render_click(add_secret_button)
|
||||
secrets_component = with_target(view, "#secrets-modal")
|
||||
form_element = element(secrets_component, "form[phx-submit='save']")
|
||||
assert has_element?(form_element)
|
||||
attrs = %{value: secret.value, hub_id: enterprise.id}
|
||||
render_submit(form_element, %{secret: attrs})
|
||||
|
||||
# Checks we received the secret created event from Enterprise
|
||||
assert_receive {:secret_created, ^secret}
|
||||
|
||||
# Checks if the secret is persisted
|
||||
assert secret in Livebook.Hubs.get_secrets()
|
||||
|
||||
# Checks if the secret exists and is inside the session,
|
||||
# then executes the code cell again and checks if the
|
||||
# secret value is what we expected.
|
||||
assert_session_secret(view, session.pid, secret)
|
||||
Session.queue_cell_evaluation(session.pid, cell_id)
|
||||
|
||||
assert_receive {:operation,
|
||||
{:add_cell_evaluation_response, _, ^cell_id, {:text, output}, _}}
|
||||
|
||||
assert output == "\e[32m\"#{secret.value}\"\e[0m"
|
||||
end
|
||||
|
||||
test "granting access for missing secret using 'Add secret' button",
|
||||
%{conn: conn, session: session, enterprise: enterprise, node: node} do
|
||||
secret =
|
||||
build(:secret,
|
||||
name: "MYSQL_PASS",
|
||||
value: "admin",
|
||||
hub_id: enterprise.id,
|
||||
readonly: true
|
||||
)
|
||||
|
||||
# Subscribe and executes the code to trigger
|
||||
# the `System.EnvError` exception and outputs the 'Add secret' button
|
||||
Session.subscribe(session.id)
|
||||
section_id = insert_section(session.pid)
|
||||
code = ~s{System.fetch_env!("LB_#{secret.name}")}
|
||||
cell_id = insert_text_cell(session.pid, section_id, :code, code)
|
||||
|
||||
Session.queue_cell_evaluation(session.pid, cell_id)
|
||||
assert_receive {:operation, {:add_cell_evaluation_response, _, ^cell_id, _, _}}
|
||||
|
||||
# Enters the session to check if the button exists
|
||||
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")
|
||||
expected_url = ~p"/sessions/#{session.id}/secrets?secret_name=#{secret.name}"
|
||||
add_secret_button = element(view, "a[href='#{expected_url}']")
|
||||
assert has_element?(add_secret_button)
|
||||
|
||||
# Persist the secret from the Enterprise
|
||||
:erpc.call(node, Enterprise.Integration, :create_secret, [secret.name, secret.value])
|
||||
|
||||
# Grant we receive the event, even with eventually delay
|
||||
assert_receive {:secret_created, ^secret}, 10_000
|
||||
|
||||
# Checks if the secret is persisted
|
||||
assert secret in Livebook.Hubs.get_secrets()
|
||||
|
||||
# Clicks the button and checks if the 'Grant access' banner
|
||||
# is being shown, so clicks it's button to set the app secret
|
||||
# to the session, allowing the user to fetches the secret.
|
||||
render_click(add_secret_button)
|
||||
secrets_component = with_target(view, "#secrets-modal")
|
||||
|
||||
assert render(secrets_component) =~
|
||||
"in #{hub_label(enterprise)}. Allow this session to access it?"
|
||||
|
||||
grant_access_button = element(secrets_component, "button", "Grant access")
|
||||
render_click(grant_access_button)
|
||||
|
||||
# Checks if the secret exists and is inside the session,
|
||||
# then executes the code cell again and checks if the
|
||||
# secret value is what we expected.
|
||||
assert_session_secret(view, session.pid, secret)
|
||||
Session.queue_cell_evaluation(session.pid, cell_id)
|
||||
|
||||
assert_receive {:operation,
|
||||
{:add_cell_evaluation_response, _, ^cell_id, {:text, output}, _}}
|
||||
|
||||
assert output == "\e[32m\"#{secret.value}\"\e[0m"
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1479,7 +1479,7 @@ defmodule LivebookWeb.SessionLiveTest do
|
|||
|
||||
describe "hubs" do
|
||||
test "selects the notebook hub", %{conn: conn, session: session} do
|
||||
hub = insert_hub(:fly)
|
||||
hub = insert_hub(:team)
|
||||
id = hub.id
|
||||
personal_id = Livebook.Hubs.Personal.id()
|
||||
|
||||
|
|
|
@ -1,50 +0,0 @@
|
|||
defmodule Livebook.EnterpriseIntegrationCase do
|
||||
use ExUnit.CaseTemplate
|
||||
|
||||
alias Livebook.EnterpriseServer
|
||||
|
||||
using do
|
||||
quote do
|
||||
use LivebookWeb.ConnCase
|
||||
|
||||
@moduletag :enterprise_integration
|
||||
|
||||
import Livebook.EnterpriseIntegrationCase,
|
||||
only: [start_new_instance: 1, stop_new_instance: 1]
|
||||
|
||||
alias Livebook.EnterpriseServer
|
||||
end
|
||||
end
|
||||
|
||||
setup_all do
|
||||
case EnterpriseServer.start() do
|
||||
{:ok, _} -> :ok
|
||||
{:error, {:already_started, _}} -> :ok
|
||||
end
|
||||
|
||||
{:ok,
|
||||
url: EnterpriseServer.url(),
|
||||
token: EnterpriseServer.token(),
|
||||
user: EnterpriseServer.user(),
|
||||
node: EnterpriseServer.get_node()}
|
||||
end
|
||||
|
||||
def start_new_instance(name) do
|
||||
suffix = Ecto.UUID.generate() |> :erlang.phash2() |> to_string()
|
||||
app_port = Enum.random(1000..9000) |> to_string()
|
||||
|
||||
{:ok, _} =
|
||||
EnterpriseServer.start(name,
|
||||
env: %{
|
||||
"DATABASE_URL" =>
|
||||
"postgres://postgres:postgres@localhost:5432/enterprise_integration_#{suffix}"
|
||||
},
|
||||
app_port: app_port
|
||||
)
|
||||
end
|
||||
|
||||
def stop_new_instance(name) do
|
||||
EnterpriseServer.disconnect(name)
|
||||
EnterpriseServer.drop_database(name)
|
||||
end
|
||||
end
|
|
@ -9,38 +9,21 @@ defmodule Livebook.Factory do
|
|||
}
|
||||
end
|
||||
|
||||
def build(:fly_metadata) do
|
||||
:fly |> build() |> Livebook.Hubs.Provider.to_metadata()
|
||||
def build(:team_metadata) do
|
||||
:team |> build() |> Livebook.Hubs.Provider.to_metadata()
|
||||
end
|
||||
|
||||
def build(:fly) do
|
||||
%Livebook.Hubs.Fly{
|
||||
id: "fly-foo-bar-baz",
|
||||
hub_name: "My Personal Hub",
|
||||
hub_emoji: "🚀",
|
||||
access_token: Livebook.Utils.random_cookie(),
|
||||
organization_id: Livebook.Utils.random_id(),
|
||||
organization_type: "PERSONAL",
|
||||
organization_name: "Foo",
|
||||
application_id: "foo-bar-baz"
|
||||
}
|
||||
end
|
||||
def build(:team) do
|
||||
org = build(:org)
|
||||
|
||||
def build(:enterprise_metadata) do
|
||||
:enterprise |> build() |> Livebook.Hubs.Provider.to_metadata()
|
||||
end
|
||||
|
||||
def build(:enterprise) do
|
||||
name = "Enteprise #{Livebook.Utils.random_short_id()}"
|
||||
|
||||
%Livebook.Hubs.Enterprise{
|
||||
id: "enterprise-#{name}",
|
||||
hub_name: name,
|
||||
%Livebook.Hubs.Team{
|
||||
id: "team-#{org.name}",
|
||||
hub_name: org.name,
|
||||
hub_emoji: "🏭",
|
||||
org_id: 1,
|
||||
user_id: 1,
|
||||
org_key_id: 1,
|
||||
teams_key: Livebook.Utils.random_id(),
|
||||
teams_key: org.teams_key,
|
||||
session_token: Livebook.Utils.random_cookie()
|
||||
}
|
||||
end
|
||||
|
|
|
@ -1,307 +0,0 @@
|
|||
defmodule Livebook.EnterpriseServer do
|
||||
@moduledoc false
|
||||
use GenServer
|
||||
|
||||
defstruct [:token, :user, :node, :port, :app_port, :url, :env]
|
||||
|
||||
@name __MODULE__
|
||||
@timeout 10_000
|
||||
@default_enterprise_dir "../enterprise"
|
||||
|
||||
def available?() do
|
||||
System.get_env("ENTERPRISE_PATH") != nil or File.exists?(@default_enterprise_dir)
|
||||
end
|
||||
|
||||
def start(name \\ @name, opts \\ []) do
|
||||
GenServer.start(__MODULE__, opts, name: name)
|
||||
end
|
||||
|
||||
def url(name \\ @name) do
|
||||
GenServer.call(name, :fetch_url, @timeout)
|
||||
end
|
||||
|
||||
def token(name \\ @name) do
|
||||
GenServer.call(name, :fetch_token, @timeout)
|
||||
end
|
||||
|
||||
def user(name \\ @name) do
|
||||
GenServer.call(name, :fetch_user, @timeout)
|
||||
end
|
||||
|
||||
def get_node(name \\ @name) do
|
||||
GenServer.call(name, :fetch_node, @timeout)
|
||||
end
|
||||
|
||||
def drop_database(name \\ @name) do
|
||||
app_port = GenServer.call(name, :fetch_port)
|
||||
state_env = GenServer.call(name, :fetch_env)
|
||||
|
||||
app_port |> env(state_env) |> mix(["ecto.drop", "--quiet"])
|
||||
end
|
||||
|
||||
def reconnect(name \\ @name) do
|
||||
GenServer.cast(name, :reconnect)
|
||||
end
|
||||
|
||||
def disconnect(name \\ @name) do
|
||||
GenServer.cast(name, :disconnect)
|
||||
end
|
||||
|
||||
# GenServer Callbacks
|
||||
|
||||
@impl true
|
||||
def init(opts) do
|
||||
state = struct!(__MODULE__, opts)
|
||||
|
||||
{:ok, %{state | node: enterprise_node()}, {:continue, :start_enterprise}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_continue(:start_enterprise, state) do
|
||||
ensure_app_dir!()
|
||||
prepare_database(state)
|
||||
|
||||
{:noreply, %{state | port: start_enterprise(state)}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:fetch_token, _from, state) do
|
||||
state = if state.token, do: state, else: create_enterprise_token(state)
|
||||
|
||||
{:reply, state.token, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:fetch_user, _from, state) do
|
||||
state = if state.user, do: state, else: create_enterprise_user(state)
|
||||
|
||||
{:reply, state.user, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:fetch_url, _from, state) do
|
||||
state = if state.app_port, do: state, else: %{state | app_port: app_port()}
|
||||
url = state.url || fetch_url(state)
|
||||
|
||||
{:reply, url, %{state | url: url}}
|
||||
end
|
||||
|
||||
def handle_call(:fetch_node, _from, state) do
|
||||
{:reply, state.node, state}
|
||||
end
|
||||
|
||||
def handle_call(:fetch_port, _from, state) do
|
||||
port = state.app_port || app_port()
|
||||
{:reply, port, state}
|
||||
end
|
||||
|
||||
def handle_call(:fetch_env, _from, state) do
|
||||
{:reply, state.env, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_cast(:reconnect, state) do
|
||||
if state.port do
|
||||
{:noreply, state}
|
||||
else
|
||||
{:noreply, %{state | port: start_enterprise(state)}}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_cast(:disconnect, state) do
|
||||
if state.port do
|
||||
Port.close(state.port)
|
||||
end
|
||||
|
||||
{:noreply, %{state | port: nil}}
|
||||
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 create_enterprise_token(state) do
|
||||
if user = state.user do
|
||||
token = call_erpc_function(state.node, :generate_user_session_token!, [user])
|
||||
%{state | token: token}
|
||||
else
|
||||
user = call_erpc_function(state.node, :create_user)
|
||||
token = call_erpc_function(state.node, :generate_user_session_token!, [user])
|
||||
|
||||
%{state | user: user, token: token}
|
||||
end
|
||||
end
|
||||
|
||||
defp create_enterprise_user(state) do
|
||||
%{state | user: call_erpc_function(state.node, :create_user)}
|
||||
end
|
||||
|
||||
defp call_erpc_function(node, function, args \\ []) do
|
||||
:erpc.call(node, Enterprise.Integration, function, args)
|
||||
end
|
||||
|
||||
defp start_enterprise(state) do
|
||||
env =
|
||||
for {key, value} <- env(state), into: [] do
|
||||
{String.to_charlist(key), String.to_charlist(value)}
|
||||
end
|
||||
|
||||
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(state, port)
|
||||
end
|
||||
|
||||
defp fetch_url(state) do
|
||||
port = state.app_port || app_port()
|
||||
"http://localhost:#{port}"
|
||||
end
|
||||
|
||||
defp prepare_database(state) do
|
||||
:ok = mix(state, ["ecto.drop", "--quiet"])
|
||||
:ok = mix(state, ["ecto.create", "--quiet"])
|
||||
:ok = mix(state, ["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", @default_enterprise_dir)
|
||||
end
|
||||
|
||||
defp app_port do
|
||||
System.get_env("ENTERPRISE_PORT", "4043")
|
||||
end
|
||||
|
||||
defp debug do
|
||||
System.get_env("ENTERPRISE_DEBUG", "false")
|
||||
end
|
||||
|
||||
defp proto do
|
||||
System.get_env("ENTERPRISE_LIVEBOOK_PROTO_PATH")
|
||||
end
|
||||
|
||||
defp wait_on_start(state, port) do
|
||||
url = state.url || fetch_url(state)
|
||||
|
||||
case :httpc.request(:get, {~c"#{url}/public/health", []}, [], []) do
|
||||
{:ok, _} ->
|
||||
port
|
||||
|
||||
{:error, _} ->
|
||||
Process.sleep(10)
|
||||
wait_on_start(state, port)
|
||||
end
|
||||
end
|
||||
|
||||
defp mix(state, args) when is_struct(state) do
|
||||
state |> env() |> mix(args)
|
||||
end
|
||||
|
||||
defp mix(env, args) do
|
||||
cmd_opts = [
|
||||
stderr_to_stdout: true,
|
||||
env: env,
|
||||
cd: app_dir(),
|
||||
into: IO.stream(:stdio, :line)
|
||||
]
|
||||
|
||||
args = ["--erl", "-elixir ansi_enabled true", "-S", "mix" | args]
|
||||
|
||||
case System.cmd(elixir_executable(), args, cmd_opts) do
|
||||
{_, 0} -> :ok
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
defp env(state) do
|
||||
app_port = state.app_port || app_port()
|
||||
env(app_port, state.env)
|
||||
end
|
||||
|
||||
defp env(app_port, state_env) do
|
||||
env = %{
|
||||
"MIX_ENV" => "livebook",
|
||||
"PORT" => to_string(app_port),
|
||||
"DEBUG" => debug()
|
||||
}
|
||||
|
||||
env = if proto(), do: Map.merge(env, %{"LIVEBOOK_PROTO_PATH" => proto()}), else: env
|
||||
|
||||
if state_env do
|
||||
Map.merge(env, state_env)
|
||||
else
|
||||
env
|
||||
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
|
|
@ -55,7 +55,6 @@ ExUnit.start(
|
|||
assert_receive_timeout: if(windows?, do: 2_500, else: 1_500),
|
||||
exclude: [
|
||||
erl_docs: erl_docs_available?,
|
||||
enterprise_integration: not Livebook.EnterpriseServer.available?(),
|
||||
teams_integration: not Livebook.TeamsServer.available?()
|
||||
]
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue