From a7701c07dee8b1c7984490d4a6d657ba45659c72 Mon Sep 17 00:00:00 2001 From: Andrew Dryga Date: Thu, 9 Nov 2023 11:41:38 -0600 Subject: [PATCH] Override default API url in local/staging envs (#2611) --- .../domain/lib/domain/config/definitions.ex | 3 +- elixir/apps/web/assets/css/scrollbar.css | 20 +-- elixir/apps/web/assets/js/hooks.js | 5 +- .../web/lib/web/components/core_components.ex | 8 +- .../web/lib/web/components/form_components.ex | 2 +- .../apps/web/lib/web/live/relay_groups/new.ex | 169 +++++++++++++++--- .../apps/web/lib/web/live/sites/new_token.ex | 151 +++++++++++----- .../test/web/live/relay_groups/new_test.exs | 2 +- elixir/config/config.exs | 2 + elixir/config/dev.exs | 4 + elixir/config/runtime.exs | 2 + terraform/environments/staging/bi.tf | 10 ++ terraform/environments/staging/main.tf | 6 +- 13 files changed, 291 insertions(+), 93 deletions(-) diff --git a/elixir/apps/domain/lib/domain/config/definitions.ex b/elixir/apps/domain/lib/domain/config/definitions.ex index d58b0a675..74fac02f2 100644 --- a/elixir/apps/domain/lib/domain/config/definitions.ex +++ b/elixir/apps/domain/lib/domain/config/definitions.ex @@ -606,10 +606,11 @@ defmodule Domain.Config.Definitions do ) ############################################## - ## Docker Registry + ## Local development and Staging Helpers ############################################## defconfig(:docker_registry, :string, default: "ghcr.io/firezone") + defconfig(:api_url_override, :string, default: nil) ############################################## ## Feature Flags diff --git a/elixir/apps/web/assets/css/scrollbar.css b/elixir/apps/web/assets/css/scrollbar.css index 8a7e7d95f..417a909a4 100644 --- a/elixir/apps/web/assets/css/scrollbar.css +++ b/elixir/apps/web/assets/css/scrollbar.css @@ -1,28 +1,10 @@ @layer utilities { .no-scrollbar::-webkit-scrollbar { - display: block; - height: 0px; - background-color: initial; - border-radius: 10px; - transition: all 2s linear; - } - - .no-scrollbar:hover::-webkit-scrollbar { - height: .5rem; + display: none; } .no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; } - - .no-scrollbar::-webkit-scrollbar-thumb { - background-color: rgb(228 228 231/var(--tw-bg-opacity)); - border-radius: 10px; - } - - .no-scrollbar::-webkit-scrollbar-track { - background-color: rgb(249 250 251); - border-radius: 5px; - } } diff --git a/elixir/apps/web/assets/js/hooks.js b/elixir/apps/web/assets/js/hooks.js index e0694d1e4..4cffbcb91 100644 --- a/elixir/apps/web/assets/js/hooks.js +++ b/elixir/apps/web/assets/js/hooks.js @@ -9,7 +9,10 @@ Hooks.Copy = { this.el.addEventListener("click", (ev) => { ev.preventDefault(); - let text = ev.currentTarget.querySelector("[data-copy]").innerHTML.trim(); + let inner_html = ev.currentTarget.querySelector("[data-copy]").innerHTML.trim(); + let doc = new DOMParser().parseFromString(inner_html, "text/html"); + let text = doc.documentElement.textContent; + let cl = ev.currentTarget.querySelector("[data-icon]").classList navigator.clipboard.writeText(text).then(() => { diff --git a/elixir/apps/web/lib/web/components/core_components.ex b/elixir/apps/web/lib/web/components/core_components.ex index 39e619a29..9811fadc0 100644 --- a/elixir/apps/web/lib/web/components/core_components.ex +++ b/elixir/apps/web/lib/web/components/core_components.ex @@ -102,10 +102,16 @@ defmodule Web.CoreComponents do attr :label, :any, required: true, doc: "Display label for the tab" end + attr :rest, :global + def tabs(assigns) do ~H"""
-
+
    + <.button style="primary" class={@class} navigate={@navigate} icon="hero-plus"> <%= render_slot(@inner_block) %> """ diff --git a/elixir/apps/web/lib/web/live/relay_groups/new.ex b/elixir/apps/web/lib/web/live/relay_groups/new.ex index 1f4029e30..934a4e231 100644 --- a/elixir/apps/web/lib/web/live/relay_groups/new.ex +++ b/elixir/apps/web/lib/web/live/relay_groups/new.ex @@ -43,16 +43,63 @@ defmodule Web.RelayGroups.New do
    Select deployment method:
    - <.tabs id="deployment-instructions"> + <.tabs id="deployment-instructions" phx-update="ignore"> <:tab id="docker-instructions" label="Docker"> - <.code_block id="code-sample-docker" class="w-full rounded-b" phx-no-format><%= docker_command(encode_group_token(@group)) %> +

    + Copy-paste this command to your server and replace PUBLIC_IP4_ADDR + and PUBLIC_IP6_ADDR + with your public IP addresses: +

    + + <.code_block id="code-sample-docker" class="w-full rounded-b" phx-no-format><%= docker_command(@env) %> <:tab id="systemd-instructions" label="Systemd"> - <.code_block id="code-sample-systemd" class="w-full rounded-b" phx-no-format><%= systemd_command(encode_group_token(@group)) %> +

    + 1. Create a systemd unit file with the following content: +

    + + <.code_block id="code-sample-systemd" class="w-full" phx-no-format>sudo nano /etc/systemd/system/firezone-relay.service + +

    + 2. Copy-paste the following content into the file and replace + PUBLIC_IP4_ADDR + and PUBLIC_IP6_ADDR + with your public IP addresses:: +

    + + <.code_block id="code-sample-systemd" class="w-full rounded-b" phx-no-format><%= systemd_command(@env) %> + +

    + 3. Save by pressing Ctrl+X, then Y, then Enter. +

    + +

    + 4. Reload systemd configuration: +

    + + <.code_block id="code-sample-systemd" class="w-full" phx-no-format>sudo systemctl daemon-reload + +

    + 5. Start the service: +

    + + <.code_block id="code-sample-systemd" class="w-full" phx-no-format>sudo systemctl start firezone-relay + +

    + 6. Enable the service to start on boot: +

    + + <.code_block id="code-sample-systemd" class="w-full" phx-no-format>sudo systemctl enable firezone-relay + +

    + 7. Check the status of the service: +

    + + <.code_block id="code-sample-systemd" class="w-full rounded-b" phx-no-format>sudo systemctl status firezone-relay -
    +
    Waiting for relay connection...
    @@ -76,7 +123,8 @@ defmodule Web.RelayGroups.New do with {:ok, group} <- Relays.create_group(attrs, socket.assigns.subject) do :ok = Relays.subscribe_for_relays_presence_in_group(group) - {:noreply, assign(socket, group: group)} + token = encode_group_token(group) + {:noreply, assign(socket, group: group, env: env(token))} else {:error, changeset} -> {:noreply, assign(socket, form: to_form(changeset))} @@ -92,30 +140,99 @@ defmodule Web.RelayGroups.New do {:noreply, socket} end - defp docker_command(secret) do - """ - docker run -d \\ - --name=firezone-relay-0 \\ - --restart=always \\ - -v /dev/net/tun:/dev/net/tun \\ - -e FIREZONE_TOKEN=#{secret} \\ - us-east1-docker.pkg.dev/firezone/firezone/relay:stable - """ + defp version do + vsn = + Application.spec(:domain) + |> Keyword.fetch!(:vsn) + |> List.to_string() + |> Version.parse!() + + "#{vsn.major}.#{vsn.minor}" end - defp systemd_command(_secret) do - """ - [Unit] - Description=zigbee2mqtt - After=network.target + defp env(token) do + api_url_override = + if api_url = Domain.Config.get_env(:web, :api_url_override) do + {"FIREZONE_API_URL", api_url} + end - [Service] - ExecStart=/usr/bin/npm start - WorkingDirectory=/opt/zigbee2mqtt - StandardOutput=inherit - StandardError=inherit - Restart=always - User=pi + [ + {"FIREZONE_ID", Ecto.UUID.generate()}, + {"FIREZONE_TOKEN", token}, + {"PUBLIC_IP4_ADDR", "YOU_MUST_SET_THIS_VALUE"}, + {"PUBLIC_IP6_ADDR", "YOU_MUST_SET_THIS_VALUE"}, + api_url_override, + {"RUST_LOG", "warn"}, + {"LOG_FORMAT", "google-cloud"} + ] + |> Enum.reject(&is_nil/1) + end + + defp docker_command(env) do + [ + "docker run -d", + "--restart=unless-stopped", + "--pull=always", + "--health-cmd=\"lsof -i UDP | grep firezone-relay\"", + "--name=firezone-relay", + "--cap-add=NET_ADMIN", + "--sysctl net.ipv4.ip_forward=1", + "--sysctl net.ipv4.conf.all.src_valid_mark=1", + "--sysctl net.ipv6.conf.all.disable_ipv6=0", + "--sysctl net.ipv6.conf.all.forwarding=1", + "--sysctl net.ipv6.conf.default.forwarding=1", + "--device=\"/dev/net/tun:/dev/net/tun\"", + Enum.map(env, fn {key, value} -> "--env #{key}=\"#{value}\"" end), + "--env FIREZONE_HOSTNAME=$(hostname)", + "#{Domain.Config.fetch_env!(:domain, :docker_registry)}/relay:#{version()}" + ] + |> List.flatten() + |> Enum.join(" \\\n ") + end + + defp systemd_command(env) do + """ + [Unit] + Description=Firezone Relay + After=network.target + + [Service] + Type=simple + #{Enum.map_join(env, "\n", fn {key, value} -> "Environment=\"#{key}=#{value}\"" end)} + ExecStartPre=/bin/sh -c ' \\ + remote_version=$(curl -Ls \\ + -H "Accept: application/vnd.github+json" \\ + -H "X-GitHub-Api-Version: 2022-11-28" \\ + https://api.github.com/repos/firezone/firezone/releases/latest | grep -oP '"'"'(?<="tag_name": ")[^"]*'"'"'); \\ + if [ -e /usr/local/bin/firezone-relay ]; then \\ + current_version=$(/usr/local/bin/firezone-relay --version | awk '"'"'{print $NF}'"'"'); \\ + else \\ + current_version=""; \\ + fi; \\ + if [ ! "$current_version" = "$remote_version" ]; then \\ + arch=$(uname -m); \\ + case $arch in \\ + aarch64) \\ + bin_url="https://github.com/firezone/firezone/releases/download/latest/relay-arm64" ;; \\ + armv7l) \\ + bin_url="https://github.com/firezone/firezone/releases/download/latest/relay-arm" ;; \\ + x86_64) \\ + bin_url="https://github.com/firezone/firezone/releases/download/latest/relay-x64" ;; \\ + *) \\ + echo "Unsupported architecture"; \\ + exit 1 ;; \\ + esac; \\ + wget -O /usr/local/bin/firezone-relay $bin_url; \\ + chmod +x /usr/local/bin/firezone-relay; \\ + fi \\ + ' + ExecStartPre=/usr/bin/chmod +x /usr/local/bin/firezone-relay + ExecStart=FIREZONE_HOSTNAME=$(hostname) /usr/local/bin/firezone-relay + Restart=always + RestartSec=3 + + [Install] + WantedBy=multi-user.target """ end diff --git a/elixir/apps/web/lib/web/live/sites/new_token.ex b/elixir/apps/web/lib/web/live/sites/new_token.ex index 9d75ef2ca..e82fb0e20 100644 --- a/elixir/apps/web/lib/web/live/sites/new_token.ex +++ b/elixir/apps/web/lib/web/live/sites/new_token.ex @@ -4,12 +4,20 @@ defmodule Web.Sites.NewToken do def mount(%{"id" => id}, _session, socket) do with {:ok, group} <- Gateways.fetch_group_by_id(id, socket.assigns.subject) do - {:ok, group} = - Gateways.update_group(%{group | tokens: []}, %{tokens: [%{}]}, socket.assigns.subject) + {group, env} = + if connected?(socket) do + {:ok, group} = + Gateways.update_group(%{group | tokens: []}, %{tokens: [%{}]}, socket.assigns.subject) - :ok = Gateways.subscribe_for_gateways_presence_in_group(group) + :ok = Gateways.subscribe_for_gateways_presence_in_group(group) - {:ok, assign(socket, group: group)} + token = encode_group_token(group) + {group, env(token)} + else + {group, nil} + end + + {:ok, assign(socket, group: group, env: env)} else {:error, _reason} -> raise Web.LiveErrors.NotFoundError end @@ -37,16 +45,58 @@ defmodule Web.Sites.NewToken do
    Select deployment method:
    - <.tabs id="deployment-instructions"> + <.tabs :if={@env} id="deployment-instructions" phx-update="ignore"> <:tab id="docker-instructions" label="Docker"> - <.code_block id="code-sample-docker" class="w-full rounded-b" phx-no-format><%= docker_command(encode_group_token(@group)) %> +

    + Copy-paste this command to your server: +

    + + <.code_block id="code-sample-docker" class="w-full rounded-b" phx-no-format><%= docker_command(@env) %> <:tab id="systemd-instructions" label="Systemd"> - <.code_block id="code-sample-systemd" class="w-full rounded-b" phx-no-format><%= systemd_command(encode_group_token(@group)) %> +

    + 1. Create a systemd unit file with the following content: +

    + + <.code_block id="code-sample-systemd" class="w-full" phx-no-format>sudo nano /etc/systemd/system/firezone-gateway.service + +

    + 2. Copy-paste the following content into the file: +

    + + <.code_block id="code-sample-systemd" class="w-full rounded-b" phx-no-format><%= systemd_command(@env) %> + +

    + 3. Save by pressing Ctrl+X, then Y, then Enter. +

    + +

    + 4. Reload systemd configuration: +

    + + <.code_block id="code-sample-systemd" class="w-full" phx-no-format>sudo systemctl daemon-reload + +

    + 5. Start the service: +

    + + <.code_block id="code-sample-systemd" class="w-full" phx-no-format>sudo systemctl start firezone-gateway + +

    + 6. Enable the service to start on boot: +

    + + <.code_block id="code-sample-systemd" class="w-full" phx-no-format>sudo systemctl enable firezone-gateway + +

    + 7. Check the status of the service: +

    + + <.code_block id="code-sample-systemd" class="w-full rounded-b" phx-no-format>sudo systemctl status firezone-gateway -
    +
    Waiting for gateway connection...
    @@ -65,30 +115,45 @@ defmodule Web.Sites.NewToken do "#{vsn.major}.#{vsn.minor}" end - defp docker_command(token) do - """ - docker run -d \\ - --restart=unless-stopped \\ - --pull=always \\ - --health-cmd="ip link | grep tun-firezone" \\ - --name=firezone-gateway \\ - --cap-add=NET_ADMIN \\ - --sysctl net.ipv4.ip_forward=1 \\ - --sysctl net.ipv4.conf.all.src_valid_mark=1 \\ - --sysctl net.ipv6.conf.all.disable_ipv6=0 \\ - --sysctl net.ipv6.conf.all.forwarding=1 \\ - --sysctl net.ipv6.conf.default.forwarding=1 \\ - --device="/dev/net/tun:/dev/net/tun" \\ - --env FIREZONE_ID="#{Ecto.UUID.generate()}" \\ - --env FIREZONE_TOKEN="#{token}" \\ - --env FIREZONE_ENABLE_MASQUERADE=1 \\ - --env FIREZONE_HOSTNAME="`hostname`" \\ - --env RUST_LOG="warn" \\ - #{Domain.Config.fetch_env!(:domain, :docker_registry)}/gateway:#{version()} - """ + defp env(token) do + api_url_override = + if api_url = Domain.Config.get_env(:web, :api_url_override) do + {"FIREZONE_API_URL", api_url} + end + + [ + {"FIREZONE_ID", Ecto.UUID.generate()}, + {"FIREZONE_TOKEN", token}, + {"FIREZONE_ENABLE_MASQUERADE", "1"}, + api_url_override, + {"RUST_LOG", "warn"} + ] + |> Enum.reject(&is_nil/1) end - defp systemd_command(token) do + defp docker_command(env) do + [ + "docker run -d", + "--restart=unless-stopped", + "--pull=always", + "--health-cmd=\"ip link | grep tun-firezone\"", + "--name=firezone-gateway", + "--cap-add=NET_ADMIN", + "--sysctl net.ipv4.ip_forward=1", + "--sysctl net.ipv4.conf.all.src_valid_mark=1", + "--sysctl net.ipv6.conf.all.disable_ipv6=0", + "--sysctl net.ipv6.conf.all.forwarding=1", + "--sysctl net.ipv6.conf.default.forwarding=1", + "--device=\"/dev/net/tun:/dev/net/tun\"", + Enum.map(env, fn {key, value} -> "--env #{key}=\"#{value}\"" end), + "--env FIREZONE_HOSTNAME=$(hostname)", + "#{Domain.Config.fetch_env!(:domain, :docker_registry)}/gateway:#{version()}" + ] + |> List.flatten() + |> Enum.join(" \\\n ") + end + + defp systemd_command(env) do """ [Unit] Description=Firezone Gateway @@ -96,34 +161,36 @@ defmodule Web.Sites.NewToken do [Service] Type=simple - Environment="FIREZONE_TOKEN=#{token}" - Environment="FIREZONE_VERSION=#{version()}" - Environment="FIREZONE_HOSTNAME=$(hostname)" - Environment="FIREZONE_ENABLE_MASQUERADE=1" + #{Enum.map_join(env, "\n", fn {key, value} -> "Environment=\"#{key}=#{value}\"" end)} ExecStartPre=/bin/sh -c ' \\ + remote_version=$(curl -Ls \\ + -H "Accept: application/vnd.github+json" \\ + -H "X-GitHub-Api-Version: 2022-11-28" \\ + https://api.github.com/repos/firezone/firezone/releases/latest | grep -oP '"'"'(?<="tag_name": ")[^"]*'"'"'); \\ if [ -e /usr/local/bin/firezone-gateway ]; then \\ - current_version=$(/usr/local/bin/firezone-gateway --version 2>&1 | awk "{print $NF}"); \\ + current_version=$(/usr/local/bin/firezone-gateway --version | awk '"'"'{print $NF}'"'"'); \\ else \\ current_version=""; \\ fi; \\ - if [ ! "$$current_version" = "${FIREZONE_VERSION}" ]; then \\ + if [ ! "$current_version" = "$remote_version" ]; then \\ arch=$(uname -m); \\ - case $$arch in \\ + case $arch in \\ aarch64) \\ - bin_url="https://github.com/firezone/firezone/releases/download/${FIREZONE_VERSION}/gateway-aarch64-unknown-linux-musl-${FIREZONE_VERSION}" ;; \\ + bin_url="https://github.com/firezone/firezone/releases/download/latest/gateway-arm64" ;; \\ armv7l) \\ - bin_url="https://github.com/firezone/firezone/releases/download/${FIREZONE_VERSION}/gateway-armv7-unknown-linux-musleabihf-${FIREZONE_VERSION}" ;; \\ + bin_url="https://github.com/firezone/firezone/releases/download/latest/gateway-arm" ;; \\ x86_64) \\ - bin_url="https://github.com/firezone/firezone/releases/download/${FIREZONE_VERSION}/gateway-x86_64-unknown-linux-musl-${FIREZONE_VERSION}" ;; \\ + bin_url="https://github.com/firezone/firezone/releases/download/latest/gateway-x64" ;; \\ *) \\ echo "Unsupported architecture"; \\ exit 1 ;; \\ esac; \\ - wget -O /usr/local/bin/firezone-gateway $$bin_url; \\ + wget -O /usr/local/bin/firezone-gateway $bin_url; \\ + chmod +x /usr/local/bin/firezone-gateway; \\ fi \\ ' ExecStartPre=/usr/bin/chmod +x /usr/local/bin/firezone-gateway - ExecStart=/usr/local/bin/firezone-gateway + ExecStart=FIREZONE_HOSTNAME=$(hostname) /usr/local/bin/firezone-gateway Restart=always RestartSec=3 diff --git a/elixir/apps/web/test/web/live/relay_groups/new_test.exs b/elixir/apps/web/test/web/live/relay_groups/new_test.exs index 5d37bd97e..9105b6511 100644 --- a/elixir/apps/web/test/web/live/relay_groups/new_test.exs +++ b/elixir/apps/web/test/web/live/relay_groups/new_test.exs @@ -123,7 +123,7 @@ defmodule Web.Live.RelayGroups.NewTest do assert html =~ "docker run" assert html =~ "Waiting for relay connection..." - token = Regex.run(~r/FIREZONE_TOKEN=([^ ]+)/, html) |> List.last() + token = Regex.run(~r/FIREZONE_TOKEN="([^ ]+)"/, html) |> List.last() assert {:ok, _token} = Domain.Relays.authorize_relay(token) group = Repo.get_by(Domain.Relays.Group, name: attrs.name) |> Repo.preload(:tokens) diff --git a/elixir/config/config.exs b/elixir/config/config.exs index 69b22c6b9..f232996d9 100644 --- a/elixir/config/config.exs +++ b/elixir/config/config.exs @@ -130,6 +130,8 @@ config :web, Web.Plugs.SecureHeaders, "connect-src 'self' https://firezone.statuspage.io" ] +config :web, api_url_override: "ws://localhost:13001/" + ############################### ##### API ##################### ############################### diff --git a/elixir/config/dev.exs b/elixir/config/dev.exs index aa9ede7f1..4c2aae7d8 100644 --- a/elixir/config/dev.exs +++ b/elixir/config/dev.exs @@ -64,6 +64,10 @@ config :web, Web.Plugs.SecureHeaders, "connect-src 'self' data: https://firezone.statuspage.io" ] +# Note: on Linux you may need to add `--add-host=host.docker.internal:host-gateway` +# to the `docker run` command. Works on Docker v20.10 and above. +config :web, api_url_override: "ws://host.docker.internal:13001/" + ############################### ##### API ##################### ############################### diff --git a/elixir/config/runtime.exs b/elixir/config/runtime.exs index bed93d42d..a3959f16c 100644 --- a/elixir/config/runtime.exs +++ b/elixir/config/runtime.exs @@ -107,6 +107,8 @@ if config_env() == :prod do cookie_signing_salt: compile_config!(:cookie_signing_salt), cookie_encryption_salt: compile_config!(:cookie_encryption_salt) + config :web, api_url_override: compile_config!(:api_url_override) + ############################### ##### API ##################### ############################### diff --git a/terraform/environments/staging/bi.tf b/terraform/environments/staging/bi.tf index d17552b8a..fba515552 100644 --- a/terraform/environments/staging/bi.tf +++ b/terraform/environments/staging/bi.tf @@ -14,6 +14,12 @@ resource "random_password" "metabase_db_password" { min_special = 1 } +# This user can also be used to connect to the Firezone database, +# but following SQL should be run manually using the Cloud SQL Proxy: +# +# GRANT SELECT ON ALL TABLES IN SCHEMA public TO metabase; +# GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO metabase; +# resource "google_sql_user" "metabase" { project = module.google-cloud-project.project.project_id @@ -85,6 +91,10 @@ module "metabase" { name = "MB_ANON_TRACKING_ENABLED" value = "false" }, + # { + # name = "MB_JETTY_PORT" + # value = "80" + # } ] health_check = { diff --git a/terraform/environments/staging/main.tf b/terraform/environments/staging/main.tf index 229b899b2..9d92f6c59 100644 --- a/terraform/environments/staging/main.tf +++ b/terraform/environments/staging/main.tf @@ -485,7 +485,11 @@ module "web" { { name = "PHOENIX_HTTP_WEB_PORT" value = "8080" - } + }, + { + name = "API_URL_OVERRIDE" + value = "wss://api.${local.tld}" + }, ], local.shared_application_environment_variables) application_labels = {