mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-10-09 21:16:26 +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 =
|
apps =
|
||||||
for node <- body["apps"]["nodes"] do
|
for node <- nodes do
|
||||||
%Fly{
|
%Fly{
|
||||||
id: "fly-" <> node["id"],
|
id: "fly-" <> node["id"],
|
||||||
access_token: access_token,
|
access_token: access_token,
|
||||||
|
@ -47,12 +47,64 @@ defmodule Livebook.Hubs.FlyClient do
|
||||||
platformVersion
|
platformVersion
|
||||||
deployed
|
deployed
|
||||||
status
|
status
|
||||||
|
secrets {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
digest
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
with {:ok, body} <- graphql(access_token, query, %{appId: app_id}) do
|
with {:ok, %{"app" => app}} <- graphql(access_token, query, %{appId: app_id}) do
|
||||||
{:ok, body["app"]}
|
{: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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -14,15 +14,22 @@ defmodule LivebookWeb.Hub.Edit.FlyComponent do
|
||||||
{:ok,
|
{:ok,
|
||||||
socket
|
socket
|
||||||
|> assign(assigns)
|
|> 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
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<div>
|
<div id={@id <> "-component"}>
|
||||||
<!-- System details -->
|
<div class="flex flex-col space-y-10">
|
||||||
<div class="flex flex-col space-y-2 pb-5">
|
<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 justify-between border border-gray-200 rounded-lg p-4">
|
||||||
<div class="flex items-center space-x-12">
|
<div class="flex items-center space-x-12">
|
||||||
<.labeled_text label="Application ID">
|
<.labeled_text label="Application ID">
|
||||||
|
@ -40,7 +47,7 @@ defmodule LivebookWeb.Hub.Edit.FlyComponent do
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col space-y-4">
|
<div class="flex flex-col space-y-2">
|
||||||
<h2 class="text-xl text-gray-800 font-semibold pb-2 border-b border-gray-200">
|
<h2 class="text-xl text-gray-800 font-semibold pb-2 border-b border-gray-200">
|
||||||
General
|
General
|
||||||
</h2>
|
</h2>
|
||||||
|
@ -103,7 +110,161 @@ defmodule LivebookWeb.Hub.Edit.FlyComponent do
|
||||||
) %>
|
) %>
|
||||||
</.form>
|
</.form>
|
||||||
</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">
|
||||||
|
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>
|
</div>
|
||||||
|
|
||||||
|
<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
|
end
|
||||||
|
|
||||||
|
@ -112,6 +273,26 @@ defmodule LivebookWeb.Hub.Edit.FlyComponent do
|
||||||
handle_event("validate", %{"fly" => %{"hub_color" => HexColor.random()}}, socket)
|
handle_event("validate", %{"fly" => %{"hub_color" => HexColor.random()}}, socket)
|
||||||
end
|
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
|
def handle_event("save", %{"fly" => params}, socket) do
|
||||||
case Fly.update_hub(socket.assigns.hub, params) do
|
case Fly.update_hub(socket.assigns.hub, params) do
|
||||||
{:ok, hub} ->
|
{:ok, hub} ->
|
||||||
|
@ -125,10 +306,49 @@ defmodule LivebookWeb.Hub.Edit.FlyComponent do
|
||||||
end
|
end
|
||||||
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
|
def handle_event("validate", %{"fly" => attrs}, socket) do
|
||||||
changeset = Fly.change_hub(socket.assigns.hub, attrs)
|
changeset = Fly.change_hub(socket.assigns.hub, attrs)
|
||||||
{:noreply, assign(socket, changeset: changeset)}
|
{:noreply, assign(socket, changeset: changeset)}
|
||||||
end
|
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)
|
defp hub_color(changeset), do: get_field(changeset, :hub_color)
|
||||||
end
|
end
|
||||||
|
|
|
@ -50,6 +50,7 @@ defmodule LivebookWeb.LiveHelpers do
|
||||||
<%= live_redirect("", to: @navigate, class: "hidden", id: "#{@id}-return") %>
|
<%= live_redirect("", to: @navigate, class: "hidden", id: "#{@id}-return") %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
class="absolute top-6 right-6 text-gray-400 flex space-x-1 items-center"
|
class="absolute top-6 right-6 text-gray-400 flex space-x-1 items-center"
|
||||||
aria_label="close modal"
|
aria_label="close modal"
|
||||||
phx-click={hide_modal(@id)}
|
phx-click={hide_modal(@id)}
|
||||||
|
|
|
@ -77,7 +77,15 @@ defmodule Livebook.Hubs.FlyClientTest do
|
||||||
"hostname" => "foo-app.fly.dev",
|
"hostname" => "foo-app.fly.dev",
|
||||||
"platformVersion" => "nomad",
|
"platformVersion" => "nomad",
|
||||||
"deployed" => true,
|
"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}}
|
response = %{"data" => %{"app" => app}}
|
||||||
|
@ -106,4 +114,112 @@ defmodule Livebook.Hubs.FlyClientTest do
|
||||||
assert {:error, "request failed with code: UNAUTHORIZED"} = FlyClient.fetch_app(hub)
|
assert {:error, "request failed with code: UNAUTHORIZED"} = FlyClient.fetch_app(hub)
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
|
@ -6,20 +6,34 @@ defmodule LivebookWeb.Hub.EditLiveTest do
|
||||||
alias Livebook.Hubs
|
alias Livebook.Hubs
|
||||||
|
|
||||||
setup do
|
setup do
|
||||||
on_exit(&Hubs.clean_hubs/0)
|
on_exit(fn ->
|
||||||
:ok
|
Hubs.clean_hubs()
|
||||||
|
end)
|
||||||
|
|
||||||
|
bypass = Bypass.open()
|
||||||
|
Application.put_env(:livebook, :fly_graphql_endpoint, "http://localhost:#{bypass.port}")
|
||||||
|
|
||||||
|
{:ok, bypass: bypass}
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "fly" do
|
describe "fly" do
|
||||||
test "updates fly", %{conn: conn} do
|
test "updates fly", %{conn: conn, bypass: bypass} do
|
||||||
hub = insert_hub(:fly, id: "fly-987654321", application_id: "987654321")
|
{:ok, pid} = Agent.start(fn -> %{fun: &fetch_app_response/2, type: :mount} end)
|
||||||
fly_bypass(hub.application_id)
|
|
||||||
|
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))
|
{:ok, view, html} = live(conn, Routes.hub_path(conn, :edit, hub.id))
|
||||||
|
|
||||||
assert html =~ "See app on Fly"
|
assert html =~ "See app on Fly"
|
||||||
assert html =~ "https://#{hub.application_id}.fly.dev"
|
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 = %{
|
attrs = %{
|
||||||
"hub_name" => "Personal Hub",
|
"hub_name" => "Personal Hub",
|
||||||
"hub_color" => "#FF00FF"
|
"hub_color" => "#FF00FF"
|
||||||
|
@ -47,7 +61,7 @@ defmodule LivebookWeb.Hub.EditLiveTest do
|
||||||
|
|
||||||
assert view
|
assert view
|
||||||
|> element("#hubs")
|
|> element("#hubs")
|
||||||
|> render() =~ "/hub/fly-987654321"
|
|> render() =~ Routes.hub_path(conn, :edit, hub.id)
|
||||||
|
|
||||||
assert view
|
assert view
|
||||||
|> element("#hubs")
|
|> element("#hubs")
|
||||||
|
@ -55,19 +69,156 @@ defmodule LivebookWeb.Hub.EditLiveTest do
|
||||||
|
|
||||||
refute Hubs.fetch_hub!(hub.id) == hub
|
refute Hubs.fetch_hub!(hub.id) == hub
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
defp fly_bypass(app_id) do
|
test "update secret", %{conn: conn, bypass: bypass} do
|
||||||
bypass = Bypass.open()
|
{:ok, pid} = Agent.start(fn -> %{fun: &fetch_app_response/2, type: :foo} end)
|
||||||
Application.put_env(:livebook, :fly_graphql_endpoint, "http://localhost:#{bypass.port}")
|
|
||||||
|
|
||||||
|
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(bypass, app_id, agent_pid) do
|
||||||
Bypass.expect(bypass, "POST", "/", fn conn ->
|
Bypass.expect(bypass, "POST", "/", fn conn ->
|
||||||
{:ok, body, conn} = Plug.Conn.read_body(conn)
|
{:ok, body, conn} = Plug.Conn.read_body(conn)
|
||||||
|
body = Jason.decode!(body)
|
||||||
|
|
||||||
response =
|
response =
|
||||||
case Jason.decode!(body) do
|
cond do
|
||||||
%{"variables" => %{"appId" => ^app_id}} -> fetch_app_response(app_id)
|
body["query"] =~ "setSecrets" ->
|
||||||
%{"variables" => %{}} -> fetch_apps_response(app_id)
|
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
|
end
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|
@ -76,29 +227,87 @@ defmodule LivebookWeb.Hub.EditLiveTest do
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp fetch_apps_response(app_id) do
|
defp fetch_app_response(app_id, type) 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
|
|
||||||
app = %{
|
app = %{
|
||||||
"id" => app_id,
|
"id" => app_id,
|
||||||
"name" => app_id,
|
"name" => app_id,
|
||||||
"hostname" => app_id <> ".fly.dev",
|
"hostname" => app_id <> ".fly.dev",
|
||||||
"platformVersion" => "nomad",
|
"platformVersion" => "nomad",
|
||||||
"deployed" => true,
|
"deployed" => true,
|
||||||
"status" => "running"
|
"status" => "running",
|
||||||
|
"secrets" => secrets(type)
|
||||||
}
|
}
|
||||||
|
|
||||||
%{"data" => %{"app" => app}}
|
%{"data" => %{"app" => app}}
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -124,11 +124,12 @@ defmodule LivebookWeb.Hub.NewLiveTest do
|
||||||
|
|
||||||
Bypass.expect(bypass, "POST", "/", fn conn ->
|
Bypass.expect(bypass, "POST", "/", fn conn ->
|
||||||
{:ok, body, conn} = Plug.Conn.read_body(conn)
|
{:ok, body, conn} = Plug.Conn.read_body(conn)
|
||||||
|
body = Jason.decode!(body)
|
||||||
|
|
||||||
response =
|
response =
|
||||||
case Jason.decode!(body) do
|
cond do
|
||||||
%{"variables" => %{"appId" => ^app_id}} -> fetch_app_response(app_id)
|
body["query"] =~ "apps" -> fetch_apps_response(app_id)
|
||||||
%{"variables" => %{}} -> fetch_apps_response(app_id)
|
body["query"] =~ "app" -> fetch_app_response(app_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|
@ -157,7 +158,21 @@ defmodule LivebookWeb.Hub.NewLiveTest do
|
||||||
"hostname" => app_id <> ".fly.dev",
|
"hostname" => app_id <> ".fly.dev",
|
||||||
"platformVersion" => "nomad",
|
"platformVersion" => "nomad",
|
||||||
"deployed" => true,
|
"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}}
|
%{"data" => %{"app" => app}}
|
||||||
|
|
Loading…
Add table
Reference in a new issue