diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 78a9dff..1657dc8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -90,7 +90,6 @@ jobs: needs: push-images uses: ./.github/workflows/docker.yml - # direct use ghcr or docker-hub image? fly-deploy: name: Deploy fly app runs-on: ubuntu-latest diff --git a/Dockerfile b/Dockerfile index 0b575f6..6c01987 100644 --- a/Dockerfile +++ b/Dockerfile @@ -69,7 +69,7 @@ FROM ${RUNNER_IMAGE} as runner # apt-get install -y libstdc++6 openssl libncurses5 locales ca-certificates \ # && apt-get clean && rm -f /var/lib/apt/lists/*_* RUN apt-get update -y && \ - apt-get install -y locales curl iputils-ping \ + apt-get install -y locales curl iputils-ping dnsutils\ && apt-get clean && rm -f /var/lib/apt/lists/*_* RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen diff --git a/Taskfile-fly-io.yml b/Taskfile-fly-io.yml index dcb89f4..021bbc9 100644 --- a/Taskfile-fly-io.yml +++ b/Taskfile-fly-io.yml @@ -16,7 +16,6 @@ tasks: st: fly status # deploy new app version up: fly deploy --build-arg GIT_COMMIT_ID=$(git log -1 --format="%H") --build-arg GIT_COMMIT_TIME=$(git log -1 --format="%ct") - reup: fly app restart log: fly logs open: fly open diff --git a/lib/hello_api.ex b/lib/hello_api.ex index be01d4f..143e13b 100644 --- a/lib/hello_api.ex +++ b/lib/hello_api.ex @@ -16,7 +16,8 @@ defmodule HelloApi do build_mode: build_mode(), build_time: build_time(), system: System.build_info(), - commit: commit() + commit: commit(), + node: Node.self() } end diff --git a/lib/my_plug.ex b/lib/my_plug.ex new file mode 100644 index 0000000..19d8847 --- /dev/null +++ b/lib/my_plug.ex @@ -0,0 +1,14 @@ +defmodule MyPlug do + import Plug.Conn + + def init(options) do + # initialize options + options + end + + def call(conn, _opts) do + conn + |> put_resp_content_type("text/plain") + |> send_resp(200, "Hello world") + end +end diff --git a/run/bandit-websocket.exs b/run/bandit-websocket.exs new file mode 100755 index 0000000..3235b04 --- /dev/null +++ b/run/bandit-websocket.exs @@ -0,0 +1,51 @@ +#!/usr/bin/env elixir + +Mix.install([:plug, :bandit, :websock_adapter]) + +defmodule EchoServer do + def init(options) do + {:ok, options} + end + + def handle_in({"ping", [opcode: :text]}, state) do + {:reply, :ok, {:text, "pong"}, state} + end + + def terminate(:timeout, state) do + {:ok, state} + end +end + +defmodule Router do + use Plug.Router + + plug Plug.Logger + plug :match + plug :dispatch + + get "/" do + send_resp(conn, 200, """ + Use the JavaScript console to interact using websockets + + sock = new WebSocket("ws://localhost:4000/websocket") + sock.addEventListener("message", console.log) + sock.addEventListener("open", () => sock.send("ping")) + """) + end + + get "/websocket" do + conn + |> WebSockAdapter.upgrade(EchoServer, [], timeout: 60_000) + |> halt() + end + + match _ do + send_resp(conn, 404, "not found") + end +end + +require Logger +webserver = {Bandit, plug: Router, scheme: :http, port: 4000} +{:ok, _} = Supervisor.start_link([webserver], strategy: :one_for_one) +Logger.info("Plug now running on http://localhost:4000") +Process.sleep(:infinity) diff --git a/run/observer.sh b/run/observer.sh new file mode 100755 index 0000000..ffae47c --- /dev/null +++ b/run/observer.sh @@ -0,0 +1,68 @@ +#!/bin/bash + +# After opening a Wireguard connection to your Fly network, run this script to +# open a BEAM Observer from your local machine to the remote server. This creates +# a local node that is clustered to a machine running on Fly. + +# In order for it to work: +# - Your wireguard connection must be up. +# - The COOKIE value must be the same as the cookie value used for your project. +# - Observer needs to be working in your local environment. That requires WxWidget support in your Erlang install. + +# When done, close Observer. It leaves you with an open IEx shell that is connected to the remote server. You can safely CTRL+C, CTRL+C to exit it. + +# COOKIE NOTE: +# ============ +# You can explicitly set the COOKIE value in the script if you prefer. That would look like this. +# +# COOKIE=YOUR-COOKIE-VALUE + +set -e + +if [ -z "$COOKIE" ]; then + echo "Set the COOKIE your project uses in the COOKIE ENV value before running this script." + exit 1 +fi + +if ! command -v jq &> /dev/null; then + echo "jq is not installed. Please install it before running this script. It is a command-line JSON processor." + exit 1 +fi + +# Get the data we need in JSON format +json_data=$(fly status --json) + +# Extract app name +app_name=$(echo "$json_data" | jq -r '.Name') + +# Extract private_ip for the first started machine +private_ip=$(echo "$json_data" | jq -r '.Machines[] | select(.state == "started") | .private_ip' | head -n 1) + +# Extract image_ref tag hash for the first started machine +image_tags=$(echo "$json_data" | jq -r '.Machines[] | select(.state == "started") | .image_ref.tag | sub("deployment-"; "")' | head -n 1) + +if [ -z "$private_ip" ]; then + echo "No instances appear to be running at this time." + exit 1 +fi + +# Assemble the full node name +FULL_NODE_NAME="${app_name}-${image_tags}@${private_ip}" +echo Attempting to connect to $FULL_NODE_NAME + +# IMPORTANT: +# ========== +# Fly.io uses an IPv6 network internally for private IPs. The BEAM needs IPv6 +# support to be enabled explicitly. +# +# The issue is, if it's enabled globally like in a `.bashrc` file, then setting +# it here essentially flips it OFF. If not set globally, then it should be set +# here. Choose the version that fits your situation. +# +# It's the `--erl "-proto_dist inet6_tcp"` portion. + +# Toggles on IPv6 support for the local node being started. +iex --erl "-proto_dist inet6_tcp" --sname my_remote --cookie ${COOKIE} -e "IO.inspect(Node.connect(:'${FULL_NODE_NAME}'), label: \"Node Connected?\"); IO.inspect(Node.list(), label: \"Connected Nodes\"); :observer.start" + +# Does NOT toggle on IPv6 support, assuming it is enabled some other way. +# iex --sname my_remote --cookie ${COOKIE} -e "IO.inspect(Node.connect(:'${FULL_NODE_NAME}'), label: \"Node Connected?\"); IO.inspect(Node.list(), label: \"Connected Nodes\"); :observer.start" \ No newline at end of file diff --git a/run/plug-cowboy.exs b/run/plug-cowboy.exs new file mode 100755 index 0000000..0ffeb86 --- /dev/null +++ b/run/plug-cowboy.exs @@ -0,0 +1,24 @@ +#!/usr/bin/env elixir + +Mix.install([:plug, :plug_cowboy]) + +defmodule MyPlug do + import Plug.Conn + + def init(options) do + # initialize options + options + end + + def call(conn, _opts) do + conn + |> put_resp_content_type("text/plain") + |> send_resp(200, "Hello world") + end +end + +require Logger +webserver = {Plug.Cowboy, plug: MyPlug, scheme: :http, options: [port: 4000]} +{:ok, _} = Supervisor.start_link([webserver], strategy: :one_for_one) +Logger.info("Plug now running on localhost:4000") +Process.sleep(:infinity) diff --git a/test/my_plug_test.exs b/test/my_plug_test.exs new file mode 100644 index 0000000..a4f91d2 --- /dev/null +++ b/test/my_plug_test.exs @@ -0,0 +1,19 @@ +defmodule MyPlugTest do + use ExUnit.Case, async: true + use Plug.Test + + @opts MyPlug.init([]) + + test "returns hello world" do + # Create a test connection + conn = conn(:get, "/hello") + + # Invoke the plug + conn = MyPlug.call(conn, @opts) + + # Assert the response and status + assert conn.state == :sent + assert conn.status == 200 + assert conn.resp_body == "Hello world" + end +end