mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-10-02 01:34:28 +08:00
Add and Fetch secrets from Fly applications (#1361)
This commit is contained in:
parent
e9e0bda94f
commit
ac71d08771
6 changed files with 724 additions and 111 deletions
|
@ -20,9 +20,9 @@ defmodule Livebook.Hubs.FlyClient do
|
|||
}
|
||||
"""
|
||||
|
||||
with {:ok, body} <- graphql(access_token, query) do
|
||||
with {:ok, %{"apps" => %{"nodes" => nodes}}} <- graphql(access_token, query) do
|
||||
apps =
|
||||
for node <- body["apps"]["nodes"] do
|
||||
for node <- nodes do
|
||||
%Fly{
|
||||
id: "fly-" <> node["id"],
|
||||
access_token: access_token,
|
||||
|
@ -47,12 +47,64 @@ defmodule Livebook.Hubs.FlyClient do
|
|||
platformVersion
|
||||
deployed
|
||||
status
|
||||
secrets {
|
||||
id
|
||||
name
|
||||
digest
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
with {:ok, body} <- graphql(access_token, query, %{appId: app_id}) do
|
||||
{:ok, body["app"]}
|
||||
with {:ok, %{"app" => app}} <- graphql(access_token, query, %{appId: app_id}) do
|
||||
{:ok, app}
|
||||
end
|
||||
end
|
||||
|
||||
def put_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 delete_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
|
||||
|
||||
|
|
|
@ -14,104 +14,285 @@ defmodule LivebookWeb.Hub.Edit.FlyComponent do
|
|||
{:ok,
|
||||
socket
|
||||
|> assign(assigns)
|
||||
|> assign(app_url: "https://#{app["hostname"]}", changeset: changeset)}
|
||||
|> assign(
|
||||
app_url: "https://#{app["hostname"]}",
|
||||
changeset: changeset,
|
||||
env_vars: app["secrets"],
|
||||
env_var_data: %{},
|
||||
operation: :new,
|
||||
valid_env_var?: false
|
||||
)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div>
|
||||
<!-- System details -->
|
||||
<div class="flex flex-col space-y-2 pb-5">
|
||||
<div class="flex items-center justify-between border border-gray-200 rounded-lg p-4">
|
||||
<div class="flex items-center space-x-12">
|
||||
<.labeled_text label="Application ID">
|
||||
<%= @hub.application_id %>
|
||||
</.labeled_text>
|
||||
<.labeled_text label="Type">
|
||||
Fly
|
||||
</.labeled_text>
|
||||
</div>
|
||||
|
||||
<a href={@app_url} class="button-base button-outlined-gray" target="_blank">
|
||||
<.remix_icon icon="dashboard-2-line" class="align-middle mr-1" />
|
||||
<span>See app on Fly</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col space-y-4">
|
||||
<h2 class="text-xl text-gray-800 font-semibold pb-2 border-b border-gray-200">
|
||||
General
|
||||
</h2>
|
||||
|
||||
<.form
|
||||
id={@id}
|
||||
class="flex flex-col mt-4 space-y-4"
|
||||
let={f}
|
||||
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">
|
||||
<div class="flex flex-col space-y-1">
|
||||
<h3 class="text-gray-800 font-semibold">
|
||||
Name
|
||||
</h3>
|
||||
<%= text_input(f, :hub_name, class: "input") %>
|
||||
<%= error_tag(f, :hub_name) %>
|
||||
<div id={@id <> "-component"}>
|
||||
<div class="flex flex-col space-y-10">
|
||||
<div class="flex flex-col space-y-2">
|
||||
<div class="flex items-center justify-between border border-gray-200 rounded-lg p-4">
|
||||
<div class="flex items-center space-x-12">
|
||||
<.labeled_text label="Application ID">
|
||||
<%= @hub.application_id %>
|
||||
</.labeled_text>
|
||||
<.labeled_text label="Type">
|
||||
Fly
|
||||
</.labeled_text>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col space-y-1">
|
||||
<h3 class="text-gray-800 font-semibold">
|
||||
Color
|
||||
</h3>
|
||||
<a href={@app_url} class="button-base button-outlined-gray" target="_blank">
|
||||
<.remix_icon icon="dashboard-2-line" class="align-middle mr-1" />
|
||||
<span>See app on Fly</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-4 items-center">
|
||||
<div
|
||||
class="border-[3px] rounded-lg p-1 flex justify-center items-center"
|
||||
style={"border-color: #{hub_color(@changeset)}"}
|
||||
>
|
||||
<div class="rounded h-5 w-5" style={"background-color: #{hub_color(@changeset)}"} />
|
||||
</div>
|
||||
<div class="relative grow">
|
||||
<%= text_input(f, :hub_color,
|
||||
class: "input",
|
||||
spellcheck: "false",
|
||||
maxlength: 7
|
||||
) %>
|
||||
<button
|
||||
class="icon-button absolute right-2 top-1"
|
||||
type="button"
|
||||
phx-click="randomize_color"
|
||||
phx-target={@myself}
|
||||
<div class="flex flex-col space-y-2">
|
||||
<h2 class="text-xl text-gray-800 font-semibold pb-2 border-b border-gray-200">
|
||||
General
|
||||
</h2>
|
||||
|
||||
<.form
|
||||
id={@id}
|
||||
class="flex flex-col mt-4 space-y-4"
|
||||
let={f}
|
||||
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">
|
||||
<div class="flex flex-col space-y-1">
|
||||
<h3 class="text-gray-800 font-semibold">
|
||||
Name
|
||||
</h3>
|
||||
<%= text_input(f, :hub_name, class: "input") %>
|
||||
<%= error_tag(f, :hub_name) %>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col space-y-1">
|
||||
<h3 class="text-gray-800 font-semibold">
|
||||
Color
|
||||
</h3>
|
||||
|
||||
<div class="flex space-x-4 items-center">
|
||||
<div
|
||||
class="border-[3px] rounded-lg p-1 flex justify-center items-center"
|
||||
style={"border-color: #{hub_color(@changeset)}"}
|
||||
>
|
||||
<.remix_icon icon="refresh-line" class="text-xl" />
|
||||
</button>
|
||||
<%= error_tag(f, :hub_color) %>
|
||||
<div class="rounded h-5 w-5" style={"background-color: #{hub_color(@changeset)}"} />
|
||||
</div>
|
||||
<div class="relative grow">
|
||||
<%= text_input(f, :hub_color,
|
||||
class: "input",
|
||||
spellcheck: "false",
|
||||
maxlength: 7
|
||||
) %>
|
||||
<button
|
||||
class="icon-button absolute right-2 top-1"
|
||||
type="button"
|
||||
phx-click="randomize_color"
|
||||
phx-target={@myself}
|
||||
>
|
||||
<.remix_icon icon="refresh-line" class="text-xl" />
|
||||
</button>
|
||||
<%= error_tag(f, :hub_color) %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= submit("Update Hub",
|
||||
class: "button-base button-blue",
|
||||
phx_disable_with: "Updating...",
|
||||
disabled: not @changeset.valid?
|
||||
) %>
|
||||
</.form>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col space-y-4">
|
||||
<h2 class="text-xl text-gray-800 font-semibold pb-2 border-b border-gray-200">
|
||||
Environment Variables
|
||||
</h2>
|
||||
|
||||
<div class="flex flex-col space-y-4">
|
||||
<%= for env_var <- @env_vars do %>
|
||||
<.environment_variable_card myself={@myself} env_var={env_var} />
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= submit("Update Hub",
|
||||
class: "button-base button-blue",
|
||||
phx_disable_with: "Updating...",
|
||||
disabled: not @changeset.valid?
|
||||
) %>
|
||||
</.form>
|
||||
<button
|
||||
class="button-base button-blue"
|
||||
type="button"
|
||||
phx-click={show_modal("environment-variable-modal")}
|
||||
>
|
||||
Add environment variable
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<.environment_variable_modal
|
||||
id="environment-variable-modal"
|
||||
on_save={hide_modal("environment-variable-modal")}
|
||||
data={@env_var_data}
|
||||
valid?={@valid_env_var?}
|
||||
myself={@myself}
|
||||
/>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp environment_variable_card(assigns) do
|
||||
~H"""
|
||||
<div
|
||||
id={"env-var-" <> @env_var["id"]}
|
||||
class="flex items-center justify-between border border-gray-200 rounded-lg p-4"
|
||||
>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 w-full">
|
||||
<div class="place-content-start">
|
||||
<.labeled_text label="Name">
|
||||
<%= @env_var["name"] %>
|
||||
</.labeled_text>
|
||||
</div>
|
||||
|
||||
<div class="flex place-content-end">
|
||||
<.labeled_text label="Created at">
|
||||
<%= @env_var["createdAt"] %>
|
||||
</.labeled_text>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center place-content-end">
|
||||
<.menu id={"env-var-#{@env_var["id"]}-menu"}>
|
||||
<:toggle>
|
||||
<button class="icon-button" aria-label="open session menu" type="button">
|
||||
<.remix_icon icon="more-2-fill" class="text-xl" />
|
||||
</button>
|
||||
</:toggle>
|
||||
<:content>
|
||||
<button
|
||||
id={"env-var-" <> @env_var["id"] <> "-edit"}
|
||||
type="button"
|
||||
phx-click={
|
||||
show_modal("environment-variable-modal")
|
||||
|> JS.push("edit", value: %{env_var: @env_var})
|
||||
}
|
||||
phx-target={@myself}
|
||||
role="menuitem"
|
||||
class="menu-item text-gray-600"
|
||||
>
|
||||
<.remix_icon icon="file-edit-line" />
|
||||
<span class="font-medium">Edit</span>
|
||||
</button>
|
||||
<button
|
||||
id={"env-var-" <> @env_var["id"] <> "-delete"}
|
||||
type="button"
|
||||
phx-click={
|
||||
with_confirm(
|
||||
JS.push("delete", value: %{env_var: @env_var}),
|
||||
title: "Delete #{@env_var["name"]}",
|
||||
description: "Are you sure you want to delete environment variable?",
|
||||
confirm_text: "Delete",
|
||||
confirm_icon: "delete-bin-6-line"
|
||||
)
|
||||
}
|
||||
phx-target={@myself}
|
||||
role="menuitem"
|
||||
class="menu-item text-red-600"
|
||||
>
|
||||
<.remix_icon icon="delete-bin-line" />
|
||||
<span class="font-medium">Delete</span>
|
||||
</button>
|
||||
</:content>
|
||||
</.menu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp environment_variable_modal(assigns) do
|
||||
~H"""
|
||||
<.modal id={@id} class="w-full max-w-lg">
|
||||
<div class="p-6 max-w-4xl flex flex-col space-y-5">
|
||||
<h3 class="text-2xl font-semibold text-gray-800">
|
||||
Add environment variable
|
||||
</h3>
|
||||
<div class="flex-col space-y-5">
|
||||
<p class="text-gray-700">
|
||||
Enter the environment variable name and its value.
|
||||
</p>
|
||||
<.form
|
||||
id="env-var-form"
|
||||
let={f}
|
||||
for={:env_var}
|
||||
phx-submit={@on_save |> JS.push("save")}
|
||||
phx-change="validate"
|
||||
autocomplete="off"
|
||||
phx-target={@myself}
|
||||
>
|
||||
<div class="flex flex-col space-y-4">
|
||||
<div>
|
||||
<div class="input-label">
|
||||
Key <span class="text-xs text-gray-500">(alphanumeric and underscore)</span>
|
||||
</div>
|
||||
<%= text_input(f, :key,
|
||||
value: @data["key"],
|
||||
class: "input",
|
||||
placeholder: "environment variable key",
|
||||
autofocus: true,
|
||||
aria_labelledby: "env-var-key",
|
||||
spellcheck: "false"
|
||||
) %>
|
||||
</div>
|
||||
<div>
|
||||
<div class="input-label">Value</div>
|
||||
<%= text_input(f, :value,
|
||||
value: @data["value"],
|
||||
class: "input",
|
||||
placeholder: "environment variable value",
|
||||
aria_labelledby: "env-var-value",
|
||||
spellcheck: "false"
|
||||
) %>
|
||||
</div>
|
||||
</div>
|
||||
<%= submit("Add environment variable",
|
||||
class: "mt-5 button-base button-blue",
|
||||
phx_disable_with: "Adding...",
|
||||
disabled: not @valid?
|
||||
) %>
|
||||
</.form>
|
||||
</div>
|
||||
</div>
|
||||
</.modal>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("randomize_color", _, socket) do
|
||||
handle_event("validate", %{"fly" => %{"hub_color" => HexColor.random()}}, socket)
|
||||
end
|
||||
|
||||
def handle_event("edit", %{"env_var" => %{"name" => name}}, socket) do
|
||||
{:noreply, assign(socket, operation: :edit, env_var_data: %{"key" => name})}
|
||||
end
|
||||
|
||||
def handle_event("delete", %{"env_var" => %{"name" => key}}, socket) do
|
||||
case FlyClient.delete_secrets(socket.assigns.hub, [key]) do
|
||||
{:ok, _} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:success, "Environment variable deleted")
|
||||
|> push_redirect(to: Routes.hub_path(socket, :edit, socket.assigns.hub.id))}
|
||||
|
||||
{:error, _} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:error, "Failed to delete environment variable")
|
||||
|> push_redirect(to: Routes.hub_path(socket, :edit, socket.assigns.hub.id))}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("save", %{"fly" => params}, socket) do
|
||||
case Fly.update_hub(socket.assigns.hub, params) do
|
||||
{:ok, hub} ->
|
||||
|
@ -125,10 +306,49 @@ defmodule LivebookWeb.Hub.Edit.FlyComponent do
|
|||
end
|
||||
end
|
||||
|
||||
def handle_event("save", %{"env_var" => params}, socket) do
|
||||
if socket.assigns.valid_env_var? do
|
||||
case FlyClient.put_secrets(socket.assigns.hub, [params]) do
|
||||
{:ok, _} ->
|
||||
message =
|
||||
if socket.assigns.operation == :new do
|
||||
"Environment variable added"
|
||||
else
|
||||
"Environment variable updated"
|
||||
end
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:success, message)
|
||||
|> push_redirect(to: Routes.hub_path(socket, :edit, socket.assigns.hub.id))}
|
||||
|
||||
{:error, _} ->
|
||||
message =
|
||||
if socket.assigns.operation == :new do
|
||||
"Failed to add environment variable"
|
||||
else
|
||||
"Failed to update environment variable"
|
||||
end
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:error, message)
|
||||
|> push_redirect(to: Routes.hub_path(socket, :edit, socket.assigns.hub.id))}
|
||||
end
|
||||
else
|
||||
{:noreply, socket}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("validate", %{"fly" => attrs}, socket) do
|
||||
changeset = Fly.change_hub(socket.assigns.hub, attrs)
|
||||
{:noreply, assign(socket, changeset: changeset)}
|
||||
end
|
||||
|
||||
def handle_event("validate", %{"env_var" => attrs}, socket) do
|
||||
valid? = String.match?(attrs["key"], ~r/^\w+$/) and attrs["value"] not in ["", nil]
|
||||
{:noreply, assign(socket, valid_env_var?: valid?, env_var_data: attrs)}
|
||||
end
|
||||
|
||||
defp hub_color(changeset), do: get_field(changeset, :hub_color)
|
||||
end
|
||||
|
|
|
@ -50,6 +50,7 @@ defmodule LivebookWeb.LiveHelpers do
|
|||
<%= live_redirect("", to: @navigate, class: "hidden", id: "#{@id}-return") %>
|
||||
<% end %>
|
||||
<button
|
||||
type="button"
|
||||
class="absolute top-6 right-6 text-gray-400 flex space-x-1 items-center"
|
||||
aria_label="close modal"
|
||||
phx-click={hide_modal(@id)}
|
||||
|
|
|
@ -77,7 +77,15 @@ defmodule Livebook.Hubs.FlyClientTest do
|
|||
"hostname" => "foo-app.fly.dev",
|
||||
"platformVersion" => "nomad",
|
||||
"deployed" => true,
|
||||
"status" => "running"
|
||||
"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}}
|
||||
|
@ -106,4 +114,112 @@ defmodule Livebook.Hubs.FlyClientTest do
|
|||
assert {:error, "request failed with code: UNAUTHORIZED"} = FlyClient.fetch_app(hub)
|
||||
end
|
||||
end
|
||||
|
||||
describe "put_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.put_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.put_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.put_secrets(hub, [%{key: "FOO", value: "BAR"}])
|
||||
end
|
||||
end
|
||||
|
||||
describe "delete_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.delete_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.delete_secrets(hub, ["FOO"])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,20 +6,34 @@ defmodule LivebookWeb.Hub.EditLiveTest do
|
|||
alias Livebook.Hubs
|
||||
|
||||
setup do
|
||||
on_exit(&Hubs.clean_hubs/0)
|
||||
:ok
|
||||
on_exit(fn ->
|
||||
Hubs.clean_hubs()
|
||||
end)
|
||||
|
||||
bypass = Bypass.open()
|
||||
Application.put_env(:livebook, :fly_graphql_endpoint, "http://localhost:#{bypass.port}")
|
||||
|
||||
{:ok, bypass: bypass}
|
||||
end
|
||||
|
||||
describe "fly" do
|
||||
test "updates fly", %{conn: conn} do
|
||||
hub = insert_hub(:fly, id: "fly-987654321", application_id: "987654321")
|
||||
fly_bypass(hub.application_id)
|
||||
test "updates fly", %{conn: conn, bypass: bypass} do
|
||||
{:ok, pid} = Agent.start(fn -> %{fun: &fetch_app_response/2, type: :mount} end)
|
||||
|
||||
app_id = Livebook.Utils.random_short_id()
|
||||
hub = insert_hub(:fly, id: "fly-#{app_id}", application_id: app_id)
|
||||
fly_bypass(bypass, app_id, pid)
|
||||
|
||||
{:ok, view, html} = live(conn, Routes.hub_path(conn, :edit, hub.id))
|
||||
|
||||
assert html =~ "See app on Fly"
|
||||
assert html =~ "https://#{hub.application_id}.fly.dev"
|
||||
|
||||
assert html =~ "Environment Variables"
|
||||
refute html =~ "FOO_ENV_VAR"
|
||||
assert html =~ "LIVEBOOK_PASSWORD"
|
||||
assert html =~ "LIVEBOOK_SECRET_KEY_BASE"
|
||||
|
||||
attrs = %{
|
||||
"hub_name" => "Personal Hub",
|
||||
"hub_color" => "#FF00FF"
|
||||
|
@ -47,7 +61,7 @@ defmodule LivebookWeb.Hub.EditLiveTest do
|
|||
|
||||
assert view
|
||||
|> element("#hubs")
|
||||
|> render() =~ "/hub/fly-987654321"
|
||||
|> render() =~ Routes.hub_path(conn, :edit, hub.id)
|
||||
|
||||
assert view
|
||||
|> element("#hubs")
|
||||
|
@ -55,19 +69,156 @@ defmodule LivebookWeb.Hub.EditLiveTest do
|
|||
|
||||
refute Hubs.fetch_hub!(hub.id) == hub
|
||||
end
|
||||
|
||||
test "add secret", %{conn: conn, bypass: bypass} do
|
||||
{:ok, pid} = Agent.start(fn -> %{fun: &fetch_app_response/2, type: :mount} end)
|
||||
|
||||
app_id = Livebook.Utils.random_short_id()
|
||||
hub = insert_hub(:fly, id: "fly-#{app_id}", application_id: app_id)
|
||||
fly_bypass(bypass, app_id, pid)
|
||||
|
||||
{:ok, view, html} = live(conn, Routes.hub_path(conn, :edit, hub.id))
|
||||
|
||||
assert html =~ "See app on Fly"
|
||||
assert html =~ "https://#{hub.application_id}.fly.dev"
|
||||
|
||||
assert html =~ "Environment Variables"
|
||||
refute html =~ "FOO_ENV_VAR"
|
||||
assert html =~ "LIVEBOOK_PASSWORD"
|
||||
assert html =~ "LIVEBOOK_SECRET_KEY_BASE"
|
||||
|
||||
view
|
||||
|> element("#env-var-form")
|
||||
|> render_change(%{"env_var" => %{"key" => "FOO_ENV_VAR", "value" => "12345"}})
|
||||
|
||||
refute view
|
||||
|> element("#env-var-form button[disabled]")
|
||||
|> has_element?()
|
||||
|
||||
:ok = Agent.update(pid, fn state -> %{state | type: :add} end)
|
||||
|
||||
assert {:ok, _view, html} =
|
||||
view
|
||||
|> element("#env-var-form")
|
||||
|> render_submit(%{"env_var" => %{"key" => "FOO_ENV_VAR", "value" => "12345"}})
|
||||
|> follow_redirect(conn)
|
||||
|
||||
assert html =~ "Environment variable added"
|
||||
assert html =~ "Environment Variables"
|
||||
assert html =~ "FOO_ENV_VAR"
|
||||
assert html =~ "LIVEBOOK_PASSWORD"
|
||||
assert html =~ "LIVEBOOK_SECRET_KEY_BASE"
|
||||
end
|
||||
|
||||
test "update secret", %{conn: conn, bypass: bypass} do
|
||||
{:ok, pid} = Agent.start(fn -> %{fun: &fetch_app_response/2, type: :foo} end)
|
||||
|
||||
old_env_var =
|
||||
:foo
|
||||
|> secrets()
|
||||
|> Enum.find(&(&1["name"] == "FOO_ENV_VAR"))
|
||||
|
||||
new_env_var =
|
||||
:updated_foo
|
||||
|> secrets()
|
||||
|> Enum.find(&(&1["name"] == "FOO_ENV_VAR"))
|
||||
|
||||
app_id = Livebook.Utils.random_short_id()
|
||||
hub = insert_hub(:fly, id: "fly-#{app_id}", application_id: app_id)
|
||||
fly_bypass(bypass, app_id, pid)
|
||||
|
||||
{:ok, view, html} = live(conn, Routes.hub_path(conn, :edit, hub.id))
|
||||
|
||||
assert html =~ "See app on Fly"
|
||||
assert html =~ "https://#{hub.application_id}.fly.dev"
|
||||
|
||||
assert html =~ "Environment Variables"
|
||||
assert html =~ "FOO_ENV_VAR"
|
||||
assert html =~ old_env_var["createdAt"]
|
||||
|
||||
view
|
||||
|> element("#env-var-#{old_env_var["id"]}-edit")
|
||||
|> render_click(%{"env_var" => old_env_var})
|
||||
|
||||
view
|
||||
|> element("#env-var-form")
|
||||
|> render_change(%{"env_var" => %{"key" => "FOO_ENV_VAR", "value" => "12345"}})
|
||||
|
||||
refute view
|
||||
|> element("#env-var-form button[disabled]")
|
||||
|> has_element?()
|
||||
|
||||
:ok = Agent.update(pid, fn state -> %{state | type: :updated_foo} end)
|
||||
|
||||
assert {:ok, _view, html} =
|
||||
view
|
||||
|> element("#env-var-form")
|
||||
|> render_submit(%{"env_var" => %{"key" => "FOO_ENV_VAR", "value" => "12345"}})
|
||||
|> follow_redirect(conn)
|
||||
|
||||
assert html =~ "Environment variable updated"
|
||||
assert html =~ "Environment Variables"
|
||||
assert html =~ "FOO_ENV_VAR"
|
||||
refute html =~ old_env_var["createdAt"]
|
||||
assert html =~ new_env_var["createdAt"]
|
||||
end
|
||||
|
||||
test "delete secret", %{conn: conn, bypass: bypass} do
|
||||
{:ok, pid} = Agent.start(fn -> %{fun: &fetch_app_response/2, type: :add} end)
|
||||
|
||||
env_var =
|
||||
:add
|
||||
|> secrets()
|
||||
|> Enum.find(&(&1["name"] == "FOO_ENV_VAR"))
|
||||
|
||||
app_id = Livebook.Utils.random_short_id()
|
||||
hub = insert_hub(:fly, id: "fly-#{app_id}", application_id: app_id)
|
||||
fly_bypass(bypass, app_id, pid)
|
||||
|
||||
{:ok, view, html} = live(conn, Routes.hub_path(conn, :edit, hub.id))
|
||||
|
||||
assert html =~ "See app on Fly"
|
||||
assert html =~ "https://#{hub.application_id}.fly.dev"
|
||||
|
||||
assert html =~ "Environment Variables"
|
||||
assert html =~ "FOO_ENV_VAR"
|
||||
assert html =~ "LIVEBOOK_PASSWORD"
|
||||
assert html =~ "LIVEBOOK_SECRET_KEY_BASE"
|
||||
|
||||
:ok = Agent.update(pid, fn state -> %{state | type: :mount} end)
|
||||
|
||||
assert {:ok, _view, html} =
|
||||
view
|
||||
|> with_target("#fly-form-component")
|
||||
|> render_click("delete", %{"env_var" => env_var})
|
||||
|> follow_redirect(conn)
|
||||
|
||||
assert html =~ "Environment variable deleted"
|
||||
assert html =~ "Environment Variables"
|
||||
refute html =~ "FOO_ENV_VAR"
|
||||
assert html =~ "LIVEBOOK_PASSWORD"
|
||||
assert html =~ "LIVEBOOK_SECRET_KEY_BASE"
|
||||
end
|
||||
end
|
||||
|
||||
defp fly_bypass(app_id) do
|
||||
bypass = Bypass.open()
|
||||
Application.put_env(:livebook, :fly_graphql_endpoint, "http://localhost:#{bypass.port}")
|
||||
|
||||
defp fly_bypass(bypass, app_id, agent_pid) do
|
||||
Bypass.expect(bypass, "POST", "/", fn conn ->
|
||||
{:ok, body, conn} = Plug.Conn.read_body(conn)
|
||||
body = Jason.decode!(body)
|
||||
|
||||
response =
|
||||
case Jason.decode!(body) do
|
||||
%{"variables" => %{"appId" => ^app_id}} -> fetch_app_response(app_id)
|
||||
%{"variables" => %{}} -> fetch_apps_response(app_id)
|
||||
cond do
|
||||
body["query"] =~ "setSecrets" ->
|
||||
put_secrets_response()
|
||||
|
||||
body["query"] =~ "unsetSecrets" ->
|
||||
delete_secrets_response()
|
||||
|
||||
true ->
|
||||
Agent.get(agent_pid, fn
|
||||
%{fun: fun, type: type} -> fun.(app_id, type)
|
||||
%{fun: fun} -> fun.()
|
||||
end)
|
||||
end
|
||||
|
||||
conn
|
||||
|
@ -76,29 +227,87 @@ defmodule LivebookWeb.Hub.EditLiveTest do
|
|||
end)
|
||||
end
|
||||
|
||||
defp fetch_apps_response(app_id) do
|
||||
app = %{
|
||||
"id" => app_id,
|
||||
"organization" => %{
|
||||
"id" => "l3soyvjmvtmwtl6l2drnbfuvltipprge",
|
||||
"name" => "Foo Bar",
|
||||
"type" => "PERSONAL"
|
||||
}
|
||||
}
|
||||
|
||||
%{"data" => %{"apps" => %{"nodes" => [app]}}}
|
||||
end
|
||||
|
||||
defp fetch_app_response(app_id) do
|
||||
defp fetch_app_response(app_id, type) do
|
||||
app = %{
|
||||
"id" => app_id,
|
||||
"name" => app_id,
|
||||
"hostname" => app_id <> ".fly.dev",
|
||||
"platformVersion" => "nomad",
|
||||
"deployed" => true,
|
||||
"status" => "running"
|
||||
"status" => "running",
|
||||
"secrets" => secrets(type)
|
||||
}
|
||||
|
||||
%{"data" => %{"app" => app}}
|
||||
end
|
||||
|
||||
defp secrets(:mount) do
|
||||
[
|
||||
%{
|
||||
"createdAt" => to_string(DateTime.utc_now()),
|
||||
"digest" => to_string(Livebook.Utils.random_cookie()),
|
||||
"id" => "123",
|
||||
"name" => "LIVEBOOK_PASSWORD"
|
||||
},
|
||||
%{
|
||||
"createdAt" => to_string(DateTime.utc_now()),
|
||||
"digest" => to_string(Livebook.Utils.random_cookie()),
|
||||
"id" => "456",
|
||||
"name" => "LIVEBOOK_SECRET_KEY_BASE"
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
defp secrets(:add) do
|
||||
[
|
||||
%{
|
||||
"createdAt" => to_string(DateTime.utc_now()),
|
||||
"digest" => to_string(Livebook.Utils.random_cookie()),
|
||||
"id" => "789",
|
||||
"name" => "FOO_ENV_VAR"
|
||||
},
|
||||
%{
|
||||
"createdAt" => to_string(DateTime.utc_now()),
|
||||
"digest" => to_string(Livebook.Utils.random_cookie()),
|
||||
"id" => "123",
|
||||
"name" => "LIVEBOOK_PASSWORD"
|
||||
},
|
||||
%{
|
||||
"createdAt" => to_string(DateTime.utc_now()),
|
||||
"digest" => to_string(Livebook.Utils.random_cookie()),
|
||||
"id" => "456",
|
||||
"name" => "LIVEBOOK_SECRET_KEY_BASE"
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
defp secrets(:foo) do
|
||||
[
|
||||
%{
|
||||
"createdAt" => "2022-08-31 14:47:39.904338Z",
|
||||
"digest" => to_string(Livebook.Utils.random_cookie()),
|
||||
"id" => "123456789",
|
||||
"name" => "FOO_ENV_VAR"
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
defp secrets(:updated_foo) do
|
||||
[
|
||||
%{
|
||||
"createdAt" => "2022-08-31 14:47:41.632669Z",
|
||||
"digest" => to_string(Livebook.Utils.random_cookie()),
|
||||
"id" => "123456789",
|
||||
"name" => "FOO_ENV_VAR"
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
defp put_secrets_response do
|
||||
%{"data" => %{"setSecrets" => %{"app" => %{"secrets" => secrets(:add)}}}}
|
||||
end
|
||||
|
||||
defp delete_secrets_response do
|
||||
%{"data" => %{"unsetSecrets" => %{"app" => %{"secrets" => secrets(:mount)}}}}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -124,11 +124,12 @@ defmodule LivebookWeb.Hub.NewLiveTest do
|
|||
|
||||
Bypass.expect(bypass, "POST", "/", fn conn ->
|
||||
{:ok, body, conn} = Plug.Conn.read_body(conn)
|
||||
body = Jason.decode!(body)
|
||||
|
||||
response =
|
||||
case Jason.decode!(body) do
|
||||
%{"variables" => %{"appId" => ^app_id}} -> fetch_app_response(app_id)
|
||||
%{"variables" => %{}} -> fetch_apps_response(app_id)
|
||||
cond do
|
||||
body["query"] =~ "apps" -> fetch_apps_response(app_id)
|
||||
body["query"] =~ "app" -> fetch_app_response(app_id)
|
||||
end
|
||||
|
||||
conn
|
||||
|
@ -157,7 +158,21 @@ defmodule LivebookWeb.Hub.NewLiveTest do
|
|||
"hostname" => app_id <> ".fly.dev",
|
||||
"platformVersion" => "nomad",
|
||||
"deployed" => true,
|
||||
"status" => "running"
|
||||
"status" => "running",
|
||||
"secrets" => [
|
||||
%{
|
||||
"createdAt" => to_string(DateTime.utc_now()),
|
||||
"digest" => to_string(Livebook.Utils.random_cookie()),
|
||||
"id" => Livebook.Utils.random_short_id(),
|
||||
"name" => "LIVEBOOK_PASSWORD"
|
||||
},
|
||||
%{
|
||||
"createdAt" => to_string(DateTime.utc_now()),
|
||||
"digest" => to_string(Livebook.Utils.random_cookie()),
|
||||
"id" => Livebook.Utils.random_short_id(),
|
||||
"name" => "LIVEBOOK_SECRET_KEY_BASE"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
%{"data" => %{"app" => app}}
|
||||
|
|
Loading…
Add table
Reference in a new issue