Repository: livebook-dev/kino
Pull request #358 (2023-10-13 10:55:53)

Node name as secret


Patch:
diff --git a/lib/assets/remote_execution_cell/main.css b/lib/assets/remote_execution_cell/main.css
index 984a225..469b857 100644
--- a/lib/assets/remote_execution_cell/main.css
+++ b/lib/assets/remote_execution_cell/main.css
@@ -67,7 +67,7 @@ button {
 
 .input--xs {
   width: auto;
-  max-width: 125px;
+  max-width: 100px;
 }
 
 .input--text {
@@ -136,6 +136,7 @@ select.input {
   border: 1px solid var(--gray-200);
   outline: none;
   padding: 8px 12px 8px 42px;
+  max-width: 175px;
 }
 
 .input-icon:hover {
@@ -168,9 +169,16 @@ select.input {
 .icon-container.inline {
   position: absolute;
   top: 1px;
+}
+
+.icon-container.inline.md {
   left: 59px;
 }
 
+.icon-container.inline.sm {
+  left: 46px;
+}
+
 .icon-container:hover {
   cursor: pointer;
   background-color: var(--gray-300);
@@ -180,6 +188,7 @@ select.input {
   border: 1px solid var(--gray-200);
   outline: none;
   padding: 8px 12px 8px 42px;
+  max-width: 175px;
 }
 
 .input-icon.required {
diff --git a/lib/assets/remote_execution_cell/main.js b/lib/assets/remote_execution_cell/main.js
index df4a443..17bea23 100644
--- a/lib/assets/remote_execution_cell/main.js
+++ b/lib/assets/remote_execution_cell/main.js
@@ -95,6 +95,10 @@ export function init(ctx, payload) {
         type: Boolean,
         default: false,
       },
+      iconOffset: {
+        type: String,
+        default: "md",
+      },
     },
 
     methods: {
@@ -115,14 +119,13 @@ export function init(ctx, payload) {
     },
 
     template: `
-      <div class="input-icon-container grow">
+      <div class="input-icon-container">
         <BaseInput
           v-if="toggleInputValue"
           :name="secretInputName"
           :label="label"
           :value="secretInputValue"
           inputClass="input input-icon"
-          :grow
           readonly
           @click="selectSecret"
           @input="$emit('update:secretInputValue', $event.target.value)"
@@ -136,12 +139,11 @@ export function init(ctx, payload) {
           type="text"
           :value="textInputValue"
           inputClass="input input-icon-text"
-          :grow
           @input="$emit('update:textInputValue', $event.target.value)"
           :required="!textInputValue && required"
           :inline="inline"
         />
-        <div :class="['icon-container', { inline: inline }]">
+        <div :class="['icon-container', { inline: inline }, iconOffset]">
           <label class="hidden-checkbox">
             <input
               type="checkbox"
@@ -177,11 +179,16 @@ export function init(ctx, payload) {
     <div class="app">
       <form @change="handleFieldChange">
         <div class="header">
-          <BaseInput
-            name="node"
+          <BaseSecret
+            textInputName="node"
+            secretInputName="node_secret"
+            toggleInputName="use_node_secret"
             label="Node"
-            v-model="fields.node"
-            inputClass="input input--md"
+            v-model:textInputValue="fields.node"
+            v-model:secretInputValue="fields.node_secret"
+            v-model:toggleInputValue="fields.use_node_secret"
+            modalTitle="Set node value"
+            iconOffset="sm"
             :inline
             :required
           />
@@ -223,7 +230,9 @@ export function init(ctx, payload) {
         field !== "assign_to" && this.updateNodeInfo();
       },
       updateNodeInfo() {
-        const node = this.fields["node"];
+        const node = this.fields["use_node_secret"]
+          ? this.fields["node_secret_value"]
+          : this.fields["node"];
         const cookie = this.fields["use_cookie_secret"]
           ? this.fields["cookie_secret_value"]
           : this.fields["cookie"];
@@ -240,9 +249,14 @@ export function init(ctx, payload) {
     setFields(fields);
   });
 
-  ctx.handleEvent("update_node_info", (cookie_secret_value) => {
-    const node = app.fields["node"];
-    ctx.setSmartCellEditorIntellisenseNode(node, cookie_secret_value);
+  ctx.handleEvent("update_node_info", (secret_value) => {
+    const node =
+      "node_secret" in secret_value ? secret_value.node_secret : getNode();
+    const cookie =
+      "cookie_secret" in secret_value
+        ? secret_value.cookie_secret
+        : getCookie();
+    ctx.setSmartCellEditorIntellisenseNode(node, cookie);
   });
 
   function setFields(fields) {
@@ -250,4 +264,18 @@ export function init(ctx, payload) {
       app.fields[field] = fields[field];
     }
   }
+
+  function getNode() {
+    const node = app.fields["use_node_secret"]
+      ? app.fields["node_secret_value"]
+      : app.fields["node"];
+    return node;
+  }
+
+  function getCookie() {
+    const cookie = app.fields["use_cookie_secret"]
+      ? app.fields["cookie_secret_value"]
+      : app.fields["cookie"];
+    return cookie;
+  }
 }
diff --git a/lib/kino/remote_execution_cell.ex b/lib/kino/remote_execution_cell.ex
index 4ca0763..7d78cea 100644
--- a/lib/kino/remote_execution_cell.ex
+++ b/lib/kino/remote_execution_cell.ex
@@ -9,25 +9,34 @@ defmodule Kino.RemoteExecutionCell do
 
   @default_code ":ok"
   @global_key __MODULE__
-  @global_attrs ["node", "cookie", "cookie_secret"]
+  @global_attrs ["node", "cookie", "cookie_secret", "node_secret"]
+  @secret_attrs ["cookie_secret", "node_secret"]
 
   @impl true
   def init(attrs, ctx) do
     {shared_cookie, shared_cookie_secret} =
       AttributeStore.get_attribute({@global_key, :cookie}, {nil, nil})
 
-    node = attrs["node"] || AttributeStore.get_attribute({@global_key, :node}) || ""
+    {shared_node, shared_node_secret} =
+      AttributeStore.get_attribute({@global_key, :node}, {nil, nil})
+
+    node_secret = attrs["node_secret"] || shared_node_secret
+    node_secret_value = node_secret && System.get_env("LB_#{node_secret}")
     cookie_secret = attrs["cookie_secret"] || shared_cookie_secret
     cookie_secret_value = cookie_secret && System.get_env("LB_#{cookie_secret}")
 
     fields = %{
       "assign_to" => attrs["assign_to"] || "",
-      "node" => node,
+      "node" => attrs["node"] || shared_node || "",
+      "node_secret" => node_secret || "",
       "cookie" => attrs["cookie"] || shared_cookie || "",
       "cookie_secret" => cookie_secret || "",
+      "use_node_secret" =>
+        if(shared_node_secret, do: true, else: Map.get(attrs, "use_node_secret", false)),
       "use_cookie_secret" =>
         if(shared_cookie, do: false, else: Map.get(attrs, "use_cookie_secret", true)),
-      "cookie_secret_value" => cookie_secret_value
+      "cookie_secret_value" => cookie_secret_value,
+      "node_secret_value" => node_secret_value
     }
 
     ctx = assign(ctx, fields: fields)
@@ -47,8 +56,8 @@ defmodule Kino.RemoteExecutionCell do
     if field in @global_attrs, do: put_shared_attr(field, value)
     fields = update_fields(field, value)
 
-    if field == "cookie_secret",
-      do: send_event(ctx, ctx.origin, "update_node_info", fields["cookie_secret_value"])
+    if field in @secret_attrs,
+      do: send_event(ctx, ctx.origin, "update_node_info", %{field => fields["#{field}_value"]})
 
     broadcast_event(ctx, "update_field", %{"fields" => fields})
 
@@ -62,7 +71,8 @@ defmodule Kino.RemoteExecutionCell do
 
   @impl true
   def to_source(%{"code" => ""}), do: ""
-  def to_source(%{"node" => ""}), do: ""
+  def to_source(%{"use_node_secret" => false, "node" => ""}), do: ""
+  def to_source(%{"use_node_secret" => true, "node_secret" => ""}), do: ""
   def to_source(%{"use_cookie_secret" => false, "cookie" => ""}), do: ""
   def to_source(%{"use_cookie_secret" => true, "cookie_secret" => ""}), do: ""
 
@@ -71,13 +81,14 @@ defmodule Kino.RemoteExecutionCell do
     to_source(attrs, code)
   end
 
-  defp to_source(%{"node" => node, "assign_to" => var} = attrs, {:ok, code}) do
+  defp to_source(%{"assign_to" => var} = attrs, {:ok, code}) do
     var = if Kino.SmartCell.valid_variable_name?(var), do: var
     call = build_call(code) |> build_var(var)
     cookie = build_set_cookie(attrs)
+    node = build_node(attrs)
 
     quote do
-      node = unquote(String.to_atom(node))
+      node = unquote(node)
       Node.set_cookie(node, unquote(cookie))
       unquote(call)
     end
@@ -115,6 +126,14 @@ defmodule Kino.RemoteExecutionCell do
 
   defp build_set_cookie(%{"cookie" => cookie}), do: String.to_atom(cookie)
 
+  defp build_node(%{"use_node_secret" => true, "node_secret" => secret}) do
+    quote do
+      String.to_atom(System.fetch_env!(unquote("LB_#{secret}")))
+    end
+  end
+
+  defp build_node(%{"node" => node}), do: String.to_atom(node)
+
   defp put_shared_attr("cookie", value) do
     AttributeStore.put_attribute({@global_key, :cookie}, {value, nil})
   end
@@ -123,8 +142,12 @@ defmodule Kino.RemoteExecutionCell do
     AttributeStore.put_attribute({@global_key, :cookie}, {nil, value})
   end
 
-  defp put_shared_attr(field, value) do
-    AttributeStore.put_attribute({@global_key, String.to_atom(field)}, value)
+  defp put_shared_attr("node", value) do
+    AttributeStore.put_attribute({@global_key, :node}, {value, nil})
+  end
+
+  defp put_shared_attr("node_secret", value) do
+    AttributeStore.put_attribute({@global_key, :node}, {nil, value})
   end
 
   defp update_fields("cookie_secret", cookie_secret) do
@@ -134,5 +157,12 @@ defmodule Kino.RemoteExecutionCell do
     }
   end
 
+  defp update_fields("node_secret", node_secret) do
+    %{
+      "node_secret" => node_secret,
+      "node_secret_value" => System.get_env("LB_#{node_secret}")
+    }
+  end
+
   defp update_fields(field, value), do: %{field => value}
 end


Test patch:
diff --git a/test/kino/remote_execution_cell_test.exs b/test/kino/remote_execution_cell_test.exs
index 8b0baf0..6f3b6ec 100644
--- a/test/kino/remote_execution_cell_test.exs
+++ b/test/kino/remote_execution_cell_test.exs
@@ -14,6 +14,8 @@ defmodule Kino.RemoteExecutionCellTest do
     "node" => "name@node",
     "cookie" => "node-cookie",
     "use_cookie_secret" => false,
+    "use_node_secret" => false,
+    "node_secret" => "",
     "cookie_secret" => ""
   }
 
@@ -155,17 +157,48 @@ defmodule Kino.RemoteExecutionCellTest do
              """
     end
 
-    test "do not generate code for an invalid secret" do
+    test "do not generate code for an invalid cookie secret" do
       attrs = %{@fields | "use_cookie_secret" => true, "cookie_secret" => ""}
       assert RemoteExecutionCell.to_source(attrs) == ""
     end
+
+    test "node name from secret" do
+      attrs = %{@fields | "use_node_secret" => true, "node_secret" => "NODE_SECRET"}
+
+      assert RemoteExecutionCell.to_source(attrs) == """
+             node = String.to_atom(System.fetch_env!("LB_NODE_SECRET"))
+             Node.set_cookie(node, :"node-cookie")
+             :erpc.call(node, fn -> :ok end)\
+             """
+    end
+
+    test "do not generate code for an invalid node secret" do
+      attrs = %{@fields | "use_node_secret" => true, "node_secret" => ""}
+      assert RemoteExecutionCell.to_source(attrs) == ""
+    end
+
+    test "node and cookie from secrets" do
+      attrs = %{
+        @fields
+        | "use_node_secret" => true,
+          "node_secret" => "NODE_SECRET",
+          "use_cookie_secret" => true,
+          "cookie_secret" => "COOKIE_SECRET"
+      }
+
+      assert RemoteExecutionCell.to_source(attrs) == """
+             node = String.to_atom(System.fetch_env!("LB_NODE_SECRET"))
+             Node.set_cookie(node, String.to_atom(System.fetch_env!("LB_COOKIE_SECRET")))
+             :erpc.call(node, fn -> :ok end)\
+             """
+    end
   end
 
   defmodule Global do
     use ExUnit.Case, async: false
 
     setup do
-      AttributeStore.put_attribute({Kino.RemoteExecutionCell, :node}, "name@node@global")
+      AttributeStore.put_attribute({Kino.RemoteExecutionCell, :node}, {"name@node@global", nil})
 
       AttributeStore.put_attribute(
         {Kino.RemoteExecutionCell, :cookie},
@@ -200,6 +233,21 @@ defmodule Kino.RemoteExecutionCellTest do
              """
     end
 
+    test "from stored attrs with node as a secret" do
+      AttributeStore.put_attribute(
+        {Kino.RemoteExecutionCell, :node},
+        {nil, "NODE_SECRET_GLOBAL"}
+      )
+
+      {_kino, source} = start_smart_cell!(RemoteExecutionCell, %{})
+
+      assert source == """
+             node = String.to_atom(System.fetch_env!("LB_NODE_SECRET_GLOBAL"))
+             Node.set_cookie(node, :"node-cookie-global")
+             :erpc.call(node, fn -> :ok end)\
+             """
+    end
+
     test "init attrs precedes stored attrs" do
       {_kino, source} = start_smart_cell!(RemoteExecutionCell, %{"node" => "name@node@attrs"})