Skip to content

Commit

Permalink
feat: update cli interface to be more elixir based (#53)
Browse files Browse the repository at this point in the history
Need to dig into the Elixir cli a bit more and see if it's possible to
pass in remote commands so I can make a cli that has commands like
`remote iex` and `remote migrate` (remote + a cli command = running that
command in remote)
  • Loading branch information
btkostner authored Aug 4, 2024
1 parent 3eefc22 commit 3d87276
Show file tree
Hide file tree
Showing 18 changed files with 615 additions and 209 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ jobs:
with:
fail_on_error: true
filter_mode: nofilter
ignore: behaviour
locale: US
reporter: ${{ github.event_name == 'pull_request' && 'github-pr-review' || 'local' }}

Expand Down Expand Up @@ -319,7 +320,7 @@ jobs:
# We run once to allow any compile warnings or things that
# would interfere with the JSON output
- name: Sobelow Sarif
run: mix sobelow --format sarif > results.sarif
run: mix sobelow --skip --format sarif > results.sarif

- name: Upload Report
uses: github/codeql-action/upload-sarif@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3
Expand Down
1 change: 1 addition & 0 deletions config/prod.exs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ config :jumar, JumarWeb.Endpoint,
# for details about using IPv6 vs IPv4 and loopback vs public addresses.
ip: {0, 0, 0, 0, 0, 0, 0, 0}
],
server: true,
url: [port: 443, scheme: "https"]

# Configures Swoosh API Client
Expand Down
19 changes: 3 additions & 16 deletions config/runtime.exs
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,6 @@ import Config
# any compile-time configuration in here, as it won't be applied.
# The block below contains prod specific runtime configuration.

# ## Using releases
#
# If you use `mix release`, you need to explicitly enable the server
# by passing the PHX_SERVER=true when you start it:
#
# PHX_SERVER=true bin/jumar start
#
# Alternatively, you can use `mix phx.gen.release` to generate a `bin/server`
# script that automatically sets the env var above.
if System.get_env("PHX_SERVER") do
config :jumar, JumarWeb.Endpoint, server: true
end

if config_env() == :prod do
database_url =
System.get_env("DATABASE_URL") ||
Expand All @@ -33,7 +20,7 @@ if config_env() == :prod do
config :jumar, Jumar.Repo,
# ssl: true,
url: database_url,
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
pool_size: "POOL_SIZE" |> System.get_env("10") |> String.to_integer(),
socket_options: maybe_ipv6

# The secret key base is used to sign/encrypt cookies and other secrets.
Expand All @@ -48,8 +35,8 @@ if config_env() == :prod do
You can generate one by calling: mix phx.gen.secret
"""

host = System.get_env("PHX_HOST") || "example.com"
port = String.to_integer(System.get_env("PORT") || "4000")
host = System.get_env("PHX_HOST", "example.com")
port = "PORT" |> System.get_env("4000") |> String.to_integer()

config :jumar, JumarWeb.Endpoint,
http: [port: port],
Expand Down
190 changes: 177 additions & 13 deletions lib/jumar_cli.ex
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
defmodule JumarCli do
@moduledoc """
The JumarCli module contains custom commands
that are used by `mix release` and the generated
Docker file. This allows us to have Elixir code for
running migrations, rollbacks, database seeds, web
servers, etc.
The JumarCli module contains custom commands that are
used by `mix release` and the generated Docker file. This
allows us to use the Elixir `OptionParser` for a more
advanced CLI interface.
"""

use Application
Expand All @@ -13,19 +12,184 @@ defmodule JumarCli do
deps: [Jumar, JumarWeb],
exports: []

@impl true
def start(type, args) do
case System.get_env("JUMAR_CMD") do
"migrate" -> JumarCli.Migrate.start(type, args)
"rollback" -> JumarCli.Migrate.start(type, args)
"server" -> JumarCli.Migrate.start(type, args)
_ -> JumarCli.Application.start(type, args)
alias JumarCli.Command

@commands [
JumarCli.Migrate,
JumarCli.Rollback,
JumarCli.Seed,
JumarCli.Server
]

@elixir_commands [
{"iex", "Starts an interactive Elixir shell for Jumar"},
{"remote", "Attach to an already running local Jumar instance"}
]

@options [
aliases: [
h: :help,
v: :version
],
switches: [
help: :boolean,
version: :boolean
]
]

@impl Application
def start(_type, _args) do
# The easiest way to pass in command line args into an
# application on the Beam VM is with environment variables.
# This is set in `rel/overlays/bin/jumar`.
response_code =
"JUMAR_CMD"
|> System.get_env("")
|> run()

case response_code do
{:ok, code} when is_integer(code) ->
# If we were given an exit code, we still need to return
# a pid for the application to start with. This ensures we
# don't get a crash or random error codes during startup.
app_fun = fn ->
Process.sleep(10)
System.halt(code)
end

{:ok, spawn(app_fun)}

{:ok, pid} when is_pid(pid) ->
{:ok, pid}
end
end

@doc """
Runs a Jumar CLI command. Will always return an ok tuple
with either a pid for a long running process, or a non
negative integer for a cli exit code.
## Examples
iex> JumarCli.run("migrate")
{:ok, 0}
"""
@spec run(String.t() | Command.args()) :: {:ok, non_neg_integer()} | {:ok, pid()}
def run(args) when is_binary(args) do
args
|> String.split(" ")
|> Enum.filter(&(&1 != ""))
|> run()
end

# If nothing is specified, we run the application
# command which includes _everything_. This allows us to
# specify this module as the Elixir application in `mix.exs`,
# and have tests and other Elixir code start the application
# as expected.
def run([]) do
JumarCli.Application.run([])
end

# If any arguments are passed in, we then use the OptionParser
# to make a more "native" feeling CLI that can parse flags and
# run subcommands.
def run(args) when is_list(args) do
{_, rest, _} = OptionParser.parse_head(args, @options)
{options, _, _} = OptionParser.parse(args, @options)

subcommand = Enum.at(rest, 0, "")
args = Enum.drop(rest, 1)
options = Enum.into(options, %{})

command_mod =
Enum.find(@commands, __MODULE__, fn c ->
Command.command_name(c) == subcommand
end)

case run(command_mod, args, options) do
:ok ->
{:ok, 0}

{:ok, pid} ->
{:ok, pid}

{:error, :help_text} ->
help_text = Command.moduledoc(command_mod)
IO.puts(help_text)
{:ok, 1}

{:error, message} ->
IO.puts("Error: #{message}")
{:ok, 1}
end
end

@spec run(Command.mod(), Command.args(), map()) :: Command.return_value()
defp run(command_mod, args, opts)

# The most basic run command involves using `--version` or `-v`
# which will _always_ print out the current application version.
defp run(_command_mod, _args, %{version: true}) do
IO.puts("Jumar #{Application.spec(:jumar, :vsn)}")
end

# The second most basic run command is `--help` or `-h` which
# will print out the help text for the given command if invoked
# with a subcommand like `seed --help`, or the main help text
# if ran without a subcommand.
defp run(command_mod, _args, %{help: true}) do
command_mod
|> Command.moduledoc()
|> IO.puts()
end

# And finally, the actual command running. If the command is matched
# to a known subcommand, run it. Otherwise we print out an error
# and help text.
defp run(__MODULE__, _args, _opts) do
IO.puts("Unknown command.")
IO.puts("")
{:error, :help_text}
end

defp run(command_mod, args, _opts) do
command_mod.run(args)
end

@doc """
Returns help text for Jumar CLI. This is returned when doing
`--help` or `-h`. Note that this help text includes some
commands that are setup in the jumar shell script outside of Elixir.
"""
def moduledoc() do
command_text =
@commands
|> Enum.map(fn c -> {Command.command_name(c), Command.shortdoc(c)} end)
|> Enum.concat(@elixir_commands)
|> Enum.sort_by(&elem(&1, 0))
|> Enum.map_join("\n ", fn {name, doc} ->
String.pad_trailing(name, 15) <> doc
end)

"""
Usage: jumar [options] command [args]
Options:
-h, --help Prints this help text
-v, --version Prints the Jumar version
Commands:
#{command_text}
"""
end

# Tell Phoenix to update the endpoint configuration
# whenever the application is updated.
@impl true
@impl Application
def config_change(changed, _new, removed) do
JumarWeb.Endpoint.config_change(changed, removed)
:ok
Expand Down
23 changes: 0 additions & 23 deletions lib/jumar_cli/application.ex

This file was deleted.

Loading

0 comments on commit 3d87276

Please sign in to comment.