Add and Fetch secrets from Fly applications (#1361)

This commit is contained in:
Alexandre de Souza 2022-09-02 12:54:28 -03:00 committed by GitHub
parent e9e0bda94f
commit ac71d08771
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 724 additions and 111 deletions

View file

@ -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

View file

@ -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

View file

@ -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)}

View file

@ -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

View file

@ -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

View file

@ -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}}