Skip to main content

scripts/livebook_demo_vm.exs

defmodule HostKit.LivebookDemoVM do
  @moduledoc false

  alias HostKit.{Host, Instance, Project}

  def main(["--" | args]), do: main(args)

  def main([command]) when command in ["ensure", "destroy", "status"] do
    apply(__MODULE__, String.to_existing_atom(command), [])
  end

  def main(_args) do
    IO.puts(:stderr, usage())
    System.halt(2)
  end

  def ensure do
    project = project()

    with {:ok, plan} <- HostKit.plan(project),
         {:ok, _results} <- HostKit.apply(plan, Keyword.merge(incus_opts(), confirm: true)) do
      print_ready()
    else
      {:error, reason} -> fail("could not ensure Livebook demo target", reason)
    end
  end

  def destroy do
    instance = instance()

    case HostKit.Instance.Backend.delete(instance, incus_opts()) do
      :ok -> :ok
      {:error, reason} -> fail("could not destroy Livebook demo target", reason)
    end
  end

  def status do
    instance = instance()

    case HostKit.Instance.Backend.read(instance, incus_opts()) do
      {:ok, %Instance{}} -> IO.puts("#{name()} is present")
      {:ok, nil} -> IO.puts("#{name()} is absent")
      {:error, reason} -> fail("could not read Livebook demo target", reason)
    end
  end

  defp project do
    Project.new(:livebook_demo)
    |> Project.add_instance(instance())
  end

  defp instance do
    host = %Host{
      name: :guest,
      hostname: "127.0.0.1",
      user: "root",
      sudo: false,
      meta: %{
        ssh: [
          user: "root",
          password: password(),
          port: ssh_port(),
          silently_accept_hosts: true
        ]
      }
    }

    Instance.new(name(), backend: :incus, image: image(), kind: kind(), lifecycle: :ephemeral)
    |> Instance.add_port(:ssh, host: ssh_port(), guest: 22)
    |> Instance.add_port(:caddy_demo, host: public_port(), guest: public_port())
    |> Instance.add_port(:phoenix_demo, host: phoenix_public_port(), guest: phoenix_public_port())
    |> Instance.add_host(host)
  end

  defp incus_opts do
    [
      incus: System.get_env("INCUS", "incus"),
      incus_sudo: env_true?("HOSTKIT_INCUS_SUDO")
    ]
  end

  defp print_ready do
    IO.puts("""

    Livebook target ready:
      Server:       127.0.0.1
      SSH user:     root
      SSH password: #{password()}
      SSH port:     #{ssh_port()}
      Caddy port:   #{public_port()}
      Phoenix port: #{phoenix_public_port()}
    """)
  end

  defp usage do
    """
    Usage: scripts/livebook_demo_vm.sh COMMAND

    Commands:
      ensure   Create/start a local Incus demo target with SSH password auth
      destroy  Delete the demo target
      status   Show target status

    Environment:
      HOSTKIT_LIVEBOOK_DEMO_VM           instance name (default: hostkit-livebook-demo)
      HOSTKIT_LIVEBOOK_DEMO_IMAGE        Incus image (default: images:ubuntu/24.04)
      HOSTKIT_LIVEBOOK_DEMO_TYPE         container or vm (default: container)
      HOSTKIT_LIVEBOOK_DEMO_SSH_PORT     host SSH port (default: 2222)
      HOSTKIT_LIVEBOOK_DEMO_PUBLIC_PORT  Caddy public demo port (default: 18080)
      HOSTKIT_LIVEBOOK_PHOENIX_PORT      Phoenix public demo port (default: 18081)
      HOSTKIT_LIVEBOOK_DEMO_PASSWORD     root password (default: hostkit-demo)
      HOSTKIT_INCUS_SUDO                 run incus through sudo: true/false (default: false)
      INCUS                              incus executable (default: incus)
    """
  end

  defp fail(message, reason) do
    IO.puts(:stderr, "#{message}: #{inspect(reason)}")
    System.halt(1)
  end

  defp name, do: env("HOSTKIT_LIVEBOOK_DEMO_VM", "hostkit-livebook-demo") |> String.to_atom()
  defp image, do: env("HOSTKIT_LIVEBOOK_DEMO_IMAGE", "images:ubuntu/24.04")
  defp password, do: env("HOSTKIT_LIVEBOOK_DEMO_PASSWORD", "hostkit-demo")
  defp ssh_port, do: env_integer("HOSTKIT_LIVEBOOK_DEMO_SSH_PORT", 2222)
  defp public_port, do: env_integer("HOSTKIT_LIVEBOOK_DEMO_PUBLIC_PORT", 18_080)
  defp phoenix_public_port, do: env_integer("HOSTKIT_LIVEBOOK_PHOENIX_PORT", 18_081)

  defp kind do
    case env("HOSTKIT_LIVEBOOK_DEMO_TYPE", "container") do
      "vm" -> :vm
      _other -> :container
    end
  end

  defp env(name, default), do: System.get_env(name, default)

  defp env_integer(name, default) do
    case System.get_env(name) do
      nil -> default
      value -> String.to_integer(value)
    end
  end

  defp env_true?(name), do: System.get_env(name) in ["1", "true", "TRUE", "yes"]
end

HostKit.LivebookDemoVM.main(System.argv())