From e1bd6aabba6abc56e2800f7627934e062fa75e95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 2 Jul 2024 11:14:51 +0200 Subject: [PATCH 1/2] Instructions for k8s agent --- .../live_editor/codemirror/languages.js | 8 + assets/package-lock.json | 59 +++++- assets/package.json | 1 + docs/deployment/clustering.md | 2 +- lib/livebook_web/components/app_components.ex | 2 +- .../components/core_components.ex | 41 +++++ .../live/app_session_live/source_component.ex | 27 +-- .../teams/deployment_group_agent_component.ex | 172 +++++++++++++++--- 8 files changed, 261 insertions(+), 51 deletions(-) diff --git a/assets/js/hooks/cell_editor/live_editor/codemirror/languages.js b/assets/js/hooks/cell_editor/live_editor/codemirror/languages.js index 43b2adedbc6..fa89d79f838 100644 --- a/assets/js/hooks/cell_editor/live_editor/codemirror/languages.js +++ b/assets/js/hooks/cell_editor/live_editor/codemirror/languages.js @@ -9,6 +9,7 @@ import { json } from "@codemirror/lang-json"; import { xml } from "@codemirror/lang-xml"; import { css } from "@codemirror/lang-css"; import { html } from "@codemirror/lang-html"; +import { yaml } from "@codemirror/lang-yaml"; import { javascript } from "@codemirror/lang-javascript"; import { erlang } from "@codemirror/legacy-modes/mode/erlang"; import { dockerFile } from "@codemirror/legacy-modes/mode/dockerfile"; @@ -29,6 +30,11 @@ const sqlDesc = LanguageDescription.of({ support: sql(), }); +const yamlDesc = LanguageDescription.of({ + name: "YAML", + support: yaml(), +}); + const jsonDesc = LanguageDescription.of({ name: "JSON", support: json(), @@ -67,6 +73,7 @@ const markdownDesc = LanguageDescription.of({ elixirDesc, erlangDesc, sqlDesc, + yamlDesc, jsonDesc, xmlDesc, cssDesc, @@ -81,6 +88,7 @@ export const languages = [ elixirDesc, erlangDesc, sqlDesc, + yamlDesc, jsonDesc, xmlDesc, cssDesc, diff --git a/assets/package-lock.json b/assets/package-lock.json index a783d32afb2..be0237ebe0e 100644 --- a/assets/package-lock.json +++ b/assets/package-lock.json @@ -14,6 +14,7 @@ "@codemirror/lang-markdown": "^6.2.3", "@codemirror/lang-sql": "^6.5.5", "@codemirror/lang-xml": "^6.0.2", + "@codemirror/lang-yaml": "^6.1.1", "@codemirror/language": "^6.10.0", "@codemirror/legacy-modes": "^6.3.3", "@codemirror/lint": "^6.4.2", @@ -1909,6 +1910,19 @@ "@lezer/xml": "^1.0.0" } }, + "node_modules/@codemirror/lang-yaml": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-yaml/-/lang-yaml-6.1.1.tgz", + "integrity": "sha512-HV2NzbK9bbVnjWxwObuZh5FuPCowx51mEfoFT9y3y+M37fA3+pbxx4I7uePuygFzDsAmCTwQSc/kXh/flab4uw==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.2.0", + "@lezer/yaml": "^1.0.0" + } + }, "node_modules/@codemirror/language": { "version": "6.10.2", "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.2.tgz", @@ -3160,9 +3174,9 @@ } }, "node_modules/@lezer/lr": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.3.14.tgz", - "integrity": "sha512-z5mY4LStlA3yL7aHT/rqgG614cfcvklS+8oFRFBYrs4YaWLJyKKM4+nN6KopToX0o9Hj6zmH6M5kinOYuy06ug==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.1.tgz", + "integrity": "sha512-CHsKq8DMKBf9b3yXPDIU4DbH+ZJd/sJdYOW2llbW/HudP5u0VS6Bfq1hLYfgU7uAYGFIyGGQIsSOXGPEErZiJw==", "dependencies": { "@lezer/common": "^1.0.0" } @@ -3186,6 +3200,16 @@ "@lezer/lr": "^1.0.0" } }, + "node_modules/@lezer/yaml": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@lezer/yaml/-/yaml-1.0.3.tgz", + "integrity": "sha512-GuBLekbw9jDBDhGur82nuwkxKQ+a3W5H0GfaAthDXcAu+XdpS43VlnxA9E9hllkpSP5ellRDKjLLj7Lu9Wr6xA==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.4.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -16181,6 +16205,19 @@ "@lezer/xml": "^1.0.0" } }, + "@codemirror/lang-yaml": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-yaml/-/lang-yaml-6.1.1.tgz", + "integrity": "sha512-HV2NzbK9bbVnjWxwObuZh5FuPCowx51mEfoFT9y3y+M37fA3+pbxx4I7uePuygFzDsAmCTwQSc/kXh/flab4uw==", + "requires": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.2.0", + "@lezer/yaml": "^1.0.0" + } + }, "@codemirror/language": { "version": "6.10.2", "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.2.tgz", @@ -17026,9 +17063,9 @@ } }, "@lezer/lr": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.3.14.tgz", - "integrity": "sha512-z5mY4LStlA3yL7aHT/rqgG614cfcvklS+8oFRFBYrs4YaWLJyKKM4+nN6KopToX0o9Hj6zmH6M5kinOYuy06ug==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.1.tgz", + "integrity": "sha512-CHsKq8DMKBf9b3yXPDIU4DbH+ZJd/sJdYOW2llbW/HudP5u0VS6Bfq1hLYfgU7uAYGFIyGGQIsSOXGPEErZiJw==", "requires": { "@lezer/common": "^1.0.0" } @@ -17052,6 +17089,16 @@ "@lezer/lr": "^1.0.0" } }, + "@lezer/yaml": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@lezer/yaml/-/yaml-1.0.3.tgz", + "integrity": "sha512-GuBLekbw9jDBDhGur82nuwkxKQ+a3W5H0GfaAthDXcAu+XdpS43VlnxA9E9hllkpSP5ellRDKjLLj7Lu9Wr6xA==", + "requires": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.4.0" + } + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", diff --git a/assets/package.json b/assets/package.json index e3058c60cc6..cf758d676d9 100644 --- a/assets/package.json +++ b/assets/package.json @@ -18,6 +18,7 @@ "@codemirror/lang-markdown": "^6.2.3", "@codemirror/lang-sql": "^6.5.5", "@codemirror/lang-xml": "^6.0.2", + "@codemirror/lang-yaml": "^6.1.1", "@codemirror/language": "^6.10.0", "@codemirror/legacy-modes": "^6.3.3", "@codemirror/lint": "^6.4.2", diff --git a/docs/deployment/clustering.md b/docs/deployment/clustering.md index 7f29cae6f26..ab5ff54017c 100644 --- a/docs/deployment/clustering.md +++ b/docs/deployment/clustering.md @@ -8,7 +8,7 @@ You may set `LIVEBOOK_CLUSTER` to one of the following values. ## `auto` -Detects the hosting platform and automatically sets up a cluster using DNS configuration. Currently the only supported platform is Fly.io. +Detects the hosting platform and automatically sets up a cluster using DNS configuration. Currently the only supported platform is Fly.io (and Kubernetes when using Livebook Teams). ## `dns:QUERY` diff --git a/lib/livebook_web/components/app_components.ex b/lib/livebook_web/components/app_components.ex index 2d647c9043f..34855738031 100644 --- a/lib/livebook_web/components/app_components.ex +++ b/lib/livebook_web/components/app_components.ex @@ -141,7 +141,7 @@ defmodule LivebookWeb.AppComponents do for more information.

- Automatic clustering is available when deploying to Fly.io. + Automatic clustering is available when deploying to Fly.io and Kubernetes.

diff --git a/lib/livebook_web/components/core_components.ex b/lib/livebook_web/components/core_components.ex index 6a086d643a2..661d1735b6c 100644 --- a/lib/livebook_web/components/core_components.ex +++ b/lib/livebook_web/components/core_components.ex @@ -521,6 +521,47 @@ defmodule LivebookWeb.CoreComponents do """ end + @doc """ + Renders a highlighted code snippet with a title and a copy button. + + ## Examples + + <.code_preview_with_title_and_copy + title=" + source_id="my-snippet" + language="elixir" + source="System.version()" /> + + """ + attr :title, :string, required: true + attr :source_id, :string, required: true + attr :language, :string, required: true + attr :source, :string, required: true + + def code_preview_with_title_and_copy(assigns) do + ~H""" +
+
+ <%= @title %> +
+ + <.icon_button + aria-label="copy source" + phx-click={JS.dispatch("lb:clipcopy", to: "##{@source_id}")} + > + <.remix_icon icon="clipboard-line" /> + + +
+
+ +
+ <.code_preview source_id={@source_id} language={@language} source={@source} /> +
+
+ """ + end + @doc """ Renders text with a tiny label. diff --git a/lib/livebook_web/live/app_session_live/source_component.ex b/lib/livebook_web/live/app_session_live/source_component.ex index ebd1647b135..902d44afef6 100644 --- a/lib/livebook_web/live/app_session_live/source_component.ex +++ b/lib/livebook_web/live/app_session_live/source_component.ex @@ -38,26 +38,13 @@ defmodule LivebookWeb.AppSessionLive.SourceComponent do

This app is built from the following notebook source:

-
-
- - <%= Session.file_name_for_download(@session) <> ".livemd" %> - -
- - <.icon_button - aria-label="copy source" - phx-click={JS.dispatch("lb:clipcopy", to: "#export-notebook-source")} - > - <.remix_icon icon="clipboard-line" /> - - -
-
-
- <.code_preview source_id="export-notebook-source" language="markdown" source={@source} /> -
-
+ + <.code_preview_with_title_and_copy + title={Session.file_name_for_download(@session) <> ".livemd"} + source_id="export-notebook-source" + language="markdown" + source={@source} + /> """ end diff --git a/lib/livebook_web/live/hub/teams/deployment_group_agent_component.ex b/lib/livebook_web/live/hub/teams/deployment_group_agent_component.ex index 213b1d5aae1..e37db9cb2e1 100644 --- a/lib/livebook_web/live/hub/teams/deployment_group_agent_component.ex +++ b/lib/livebook_web/live/hub/teams/deployment_group_agent_component.ex @@ -95,17 +95,13 @@ defmodule LivebookWeb.Hub.Teams.DeploymentGroupAgentComponent do to set the environment variables as secrets, if applicable. Below is an example calling Docker CLI directly, adapt it as necessary.

-
-
- CLI -
- - <.code_preview - source_id="agent-dockerfile-source" - source={@instructions.docker_instructions} - language="shell" - /> -
+ + <.code_preview_with_title_and_copy + title="CLI" + source_id="agent-dockerfile-source" + source={@instructions.docker_instructions} + language="shell" + /> <:tab id="fly_io" label="Fly.io"> @@ -113,17 +109,38 @@ defmodule LivebookWeb.Hub.Teams.DeploymentGroupAgentComponent do

Deploy an app server to Fly.io with a few commands.

-
-
- CLI -
- - <.code_preview - source_id="agent-dockerfile-source" - source={@instructions.fly_instructions} - language="shell" - /> -
+ + <.code_preview_with_title_and_copy + title="CLI" + source_id="agent-fly-source" + source={@instructions.fly_instructions} + language="shell" + /> + + + <:tab id="k8s" label="Kubernetes"> +
+

+ Deploy an app server to Kubernetes. First save the following Kubernetes resource file to disk: +

+ + <.code_preview_with_title_and_copy + title="livebook.yml" + source_id="agent-k8s-source" + source={@instructions.k8s_instructions} + language="yaml" + /> + +

+ Now run the following shell command: +

+ + <.code_preview_with_title_and_copy + title="CLI" + source_id="agent-k8s-cli" + source="kubectl apply -f livebook.yml" + language="shell" + />
@@ -178,7 +195,8 @@ defmodule LivebookWeb.Hub.Teams.DeploymentGroupAgentComponent do %{ docker_instructions: docker_instructions(image, env), - fly_instructions: fly_instructions(image, env, hub.hub_name, deployment_group.name) + fly_instructions: fly_instructions(image, env, hub.hub_name, deployment_group.name), + k8s_instructions: k8s_instructions(image, env) } end @@ -213,4 +231,112 @@ defmodule LivebookWeb.Hub.Teams.DeploymentGroupAgentComponent do fly deploy --ha=false """ end + + defp k8s_instructions(image, env) do + {secrets, envs} = + Map.split( + Map.new(env), + ~w(LIVEBOOK_TEAMS_KEY LIVEBOOK_TEAMS_AUTH LIVEBOOK_SECRET_KEY_BASE LIVEBOOK_COOKIE) + ) + + # We replace auto by the cluster setting. + {replicas, envs} = + case envs do + %{"LIVEBOOK_CLUSTER" => "auto"} -> {2, Map.delete(envs, "LIVEBOOK_CLUSTER")} + %{} -> {1, envs} + end + + envs = + Map.put_new( + envs, + "LIVEBOOK_CLUSTER", + "dns:livebook-headless.$(POD_NAMESPACE).svc.cluster.local" + ) + + k8s_instructions_template(image, envs, secrets, replicas) + end + + require EEx + + EEx.function_from_string( + :defp, + :k8s_instructions_template, + """ + apiVersion: v1 + kind: Service + metadata: + name: livebook-headless + spec: + clusterIP: None + selector: + app: livebook + + --- + + apiVersion: v1 + kind: Service + metadata: + name: livebook-loadbalancer + spec: + type: LoadBalancer + ports: + - port: 80 + targetPort: 8080 + selector: + app: livebook + + --- + + apiVersion: apps/v1 + kind: Deployment + metadata: + name: livebook + spec: + replicas: <%= replicas %> + selector: + matchLabels: + app: livebook + template: + metadata: + labels: + app: livebook + spec: + containers: + - name: livebook + image: <%= image %> + ports: + - containerPort: 8080 + env: + - name: POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: LIVEBOOK_NODE + value: "livebook@$(POD_IP)"<%= for {k, v} <- envs do %> + - name: <%= k %> + value: <%= inspect(v) %><% end %><%= for {k, _} <- secrets do %> + - name: <%= k %> + valueFrom: + secretKeyRef: + name: livebook-secret + key: <%= k %><% end %> + + --- + + apiVersion: v1 + kind: Secret + metadata: + name: livebook-secret + namespace: livebook-namespace + type: Opaque + data: + # LIVEBOOK_PASSWORD: <%= for {k, v} <- secrets do %> + <%= k %>: <%= Base.encode64(v) %><% end %> + """, + [:image, :envs, :secrets, :replicas] + ) end From 0d78307078b84bce749206405e53ad49e3c41347 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 2 Jul 2024 11:24:05 +0200 Subject: [PATCH 2/2] Update lib/livebook_web/components/core_components.ex MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jonatan KÅ‚osko --- lib/livebook_web/components/core_components.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/livebook_web/components/core_components.ex b/lib/livebook_web/components/core_components.ex index 661d1735b6c..681c0b19b3f 100644 --- a/lib/livebook_web/components/core_components.ex +++ b/lib/livebook_web/components/core_components.ex @@ -555,7 +555,7 @@ defmodule LivebookWeb.CoreComponents do -
+
<.code_preview source_id={@source_id} language={@language} source={@source} />