Initial commit

This commit is contained in:
Mikko Ahlroth 2018-02-25 00:25:21 +02:00
commit 92767d41a2
15 changed files with 392 additions and 0 deletions

4
.formatter.exs Normal file
View file

@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

26
.gitignore vendored Normal file
View file

@ -0,0 +1,26 @@
# The directory Mix will write compiled artifacts to.
/_build/
# If you run "mix test --cover", coverage assets end up here.
/cover/
# The directory Mix downloads your dependencies sources to.
/deps/
# Where 3rd-party dependencies like ExDoc output generated docs.
/doc/
# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch
# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez
# Ignore package tarball (built via "mix hex.build").
triggerer-*.tar
# Testing scripts
test*.sh

6
.on-save.json Normal file
View file

@ -0,0 +1,6 @@
[
{
"files": "**/*.{ex,exs}",
"command": "mix format ${srcFile}"
}
]

45
README.md Normal file
View file

@ -0,0 +1,45 @@
# Triggerer
Triggerer is a small application that triggers execution of scripts when given endpoints are hit
with GET requests. It contains no permissions handling, no authentication, no request limiting. So
these must be handled in some other way, like with an Nginx reverse proxy.
Should work on any \*nix system that has `sh`.
## Installation
1. Clone repo
2. `mix deps.get`
3. Build release with `MIX_ENV=prod COOKIE=yourcookie mix release --env=prod`
4. Run release in your preferred way, setting environment according to the next section
## Environment variables
To bind to a certain port (instead of default `6000`), add env `PORT=1337`.
Configuration of endpoints and scripts is done via environment variables. Environment variables
are set in the following format:
```
TRIGGERER_FOO=/path/to/bar.sh
```
The variable consists of `TRIGGERER_` prefix, after which is the name of the endpoint. The value is
the path to the script to execute. The above variable would result in an endpoint `/foo`, which would
execute `/path/to/bar.sh`.
The endpoint path is transformed to lowercase and underscores are replaced with dashes. See the
following examples:
* `TRIGGERER_FOO``/foo`
* `TRIGGERER_CODE_STATS_BETA``/code-stats-beta`
* `TRIGGERER_AAA_``/aaa-`
The path to the script can be absolute or relative, as long as it can be reached from the current
working dir.
## Return value
If the script was triggered, 200 OK is returned. If the script is not found or the request path is
wrong, 404 File not found is returned. If there is an error in script execution, 500 Internal server
error is returned and the error is logged.

34
config/config.exs Normal file
View file

@ -0,0 +1,34 @@
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
use Mix.Config
# This configuration is loaded before any dependency and is restricted
# to this project. If another project depends on this project, this
# file won't be loaded nor affect the parent project. For this reason,
# if you want to provide default values for your application for
# 3rd-party users, it should be done in your "mix.exs" file.
# You can configure your application as:
#
# config :triggerer, key: :value
#
# and access this configuration in your application as:
#
# Application.get_env(:triggerer, :key)
#
# You can also configure a 3rd-party app:
#
# config :logger, level: :info
#
# It is also possible to import configuration files, relative to this
# directory. For example, you can emulate configuration per environment
# by uncommenting the line below and defining dev.exs, test.exs and such.
# Configuration from the imported file will override the ones defined
# here (which is why it is important to import them last).
#
env_config = "#{Mix.env()}.exs"
if File.exists?(Path.join([__DIR__, env_config])) do
import_config(env_config)
end

7
config/dev.exs Normal file
View file

@ -0,0 +1,7 @@
use Mix.Config
secret = "dev.secret.exs"
if File.exists?(Path.join([__DIR__, secret])) do
import_config(secret)
end

10
config/prod.exs Normal file
View file

@ -0,0 +1,10 @@
use Mix.Config
# Do not print debug messages in production
config :logger, level: :error
secret = "prod.secret.exs"
if File.exists?(Path.join([__DIR__, secret])) do
import_config(secret)
end

49
lib/router.ex Normal file
View file

@ -0,0 +1,49 @@
defmodule Triggerer.Router do
@moduledoc """
Router dispatching requests to the correct handlers.
"""
use Plug.Router
use Plug.ErrorHandler
require Logger
plug(Plug.Logger)
plug(:match)
plug(:dispatch)
paths =
quote do
Triggerer.get_envs()
end
match "/:path" do
case Map.get(unquote(paths), path) do
nil ->
route_not_found(conn)
script ->
case Triggerer.run(script) do
:ok ->
send_resp(conn, 200, "OK")
{:error, :file_not_found} ->
send_resp(conn, 404, "File not found.")
error ->
Logger.error("Unknown error: #{inspect(error)}")
send_resp(conn, 500, "Unknown error.")
end
end
end
match _ do
route_not_found(conn)
end
defp route_not_found(conn), do: send_resp(conn, 404, "Route not found.")
defp handle_errors(conn, data) do
Logger.error(inspect(data))
send_resp(conn, conn.status, "Something went wrong.")
end
end

81
lib/triggerer.ex Normal file
View file

@ -0,0 +1,81 @@
defmodule Triggerer do
require Logger
@doc """
Get all matching environment variables and their values. Returns a map where keys are the transformed
paths and values are the given script paths.
## Examples
In these examples, sample env is given as argument. If no argument is given, system env is used.
iex> Triggerer.get_envs(%{"TRIGGERER_FOO" => "./test.sh"})
%{"foo" => "./test.sh"}
iex> Triggerer.get_envs(%{})
%{}
iex> Triggerer.get_envs(%{"WRONG_FORMAT" => "no", "TRIGGERER_BAR_" => "baz", "TRIGGERER_" => "no", "TRIGGERER_GO_WILD" => "/bin/true"})
%{"bar-" => "baz", "go-wild" => "/bin/true"}
"""
@spec get_envs(%{optional(String.t()) => String.t()}) :: map
def get_envs(envs \\ nil) do
envs = envs || System.get_env()
envs
|> Map.keys()
|> Enum.reduce(%{}, fn
"TRIGGERER_" <> rest = key, acc when rest != "" ->
path = transform_path(rest)
Map.put(acc, path, envs[key])
_, acc ->
acc
end)
end
@doc """
Transform a given path from environment variable style to endpoint style. That is, downcase it and
replace underscores with dashes.
## Examples
iex> Triggerer.transform_path("YKSI_KAKSI")
"yksi-kaksi"
iex> Triggerer.transform_path("FOO")
"foo"
iex> Triggerer.transform_path("ÄNKYRÄ_KÖNKYRÄ_")
"änkyrä-könkyrä-"
"""
@spec transform_path(String.t()) :: String.t()
def transform_path(path) when is_binary(path) do
String.split(path, "_")
|> Enum.map(&String.downcase/1)
|> Enum.join("-")
end
@doc """
Run given script or return an error.
"""
@spec run(String.t()) :: :ok | {:error, atom}
def run(script) do
runner = fn script ->
try do
{output, status} = System.cmd("sh", ["-c", script])
Logger.debug("Executed #{script} with return status #{status}. Output:\n\n#{output}")
rescue
err -> Logger.error("#{script} execution failed with: #{inspect(err)}")
end
end
if File.exists?(script) do
{:ok, pid} = Task.start(fn -> runner.(script) end)
Logger.debug("Started task #{script} with pid #{inspect(pid)}")
:ok
else
{:error, :file_not_found}
end
end
end

View file

@ -0,0 +1,30 @@
defmodule Triggerer.Application do
# See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications
@moduledoc false
use Application
def start(_type, _args) do
port =
case System.get_env("PORT") do
nil -> 6000
port when is_binary(port) -> String.to_integer(port)
end
# List all child processes to be supervised
children = [
Plug.Adapters.Cowboy.child_spec(
:http,
Triggerer.Router,
[],
port: port
)
]
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: Triggerer.Supervisor]
Supervisor.start_link(children, opts)
end
end

30
mix.exs Normal file
View file

@ -0,0 +1,30 @@
defmodule Triggerer.MixProject do
use Mix.Project
def project do
[
app: :triggerer,
version: "1.0.0",
elixir: "~> 1.6",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger],
mod: {Triggerer.Application, []}
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:plug, "~> 1.4.5"},
{:distillery, "~> 1.5"},
{:cowboy, "~> 1.1"}
]
end
end

8
mix.lock Normal file
View file

@ -0,0 +1,8 @@
%{
"cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"},
"cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], [], "hexpm"},
"distillery": {:hex, :distillery, "1.5.2", "eec18b2d37b55b0bcb670cf2bcf64228ed38ce8b046bb30a9b636a6f5a4c0080", [:mix], [], "hexpm"},
"mime": {:hex, :mime, "1.2.0", "78adaa84832b3680de06f88f0997e3ead3b451a440d183d688085be2d709b534", [:mix], [], "hexpm"},
"plug": {:hex, :plug, "1.4.5", "7b13869283fff6b8b21b84b8735326cc012c5eef8607095dc6ee24bd0a273d8e", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"},
"ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], [], "hexpm"},
}

57
rel/config.exs Normal file
View file

@ -0,0 +1,57 @@
# Import all plugins from `rel/plugins`
# They can then be used by adding `plugin MyPlugin` to
# either an environment, or release definition, where
# `MyPlugin` is the name of the plugin module.
Path.join(["rel", "plugins", "*.exs"])
|> Path.wildcard()
|> Enum.map(&Code.eval_file(&1))
use Mix.Releases.Config,
# This sets the default release built by `mix release`
default_release: :default,
# This sets the default environment used by `mix release`
default_environment: Mix.env()
# For a full list of config options for both releases
# and environments, visit https://hexdocs.pm/distillery/configuration.html
# You may define one or more environments in this file,
# an environment's settings will override those of a release
# when building in that environment, this combination of release
# and environment configuration is called a profile
environment :dev do
# If you are running Phoenix, you should make sure that
# server: true is set and the code reloader is disabled,
# even in dev mode.
# It is recommended that you build with MIX_ENV=prod and pass
# the --env flag to Distillery explicitly if you want to use
# dev mode.
set(dev_mode: true)
set(include_erts: false)
set(cookie: :"jtfxziV4Hbzkc]9>uy,wHr6OuTroy?<]QCm)b8JEwJScvK/42{f]=85}rr7bO)&r")
end
environment :prod do
set(include_erts: true)
set(include_src: false)
set(
cookie: :crypto.hash(:sha256, System.get_env("COOKIE")) |> Base.encode16() |> String.to_atom()
)
end
# You may define one or more releases in this file.
# If you have not set a default release, or selected one
# when running `mix release`, the first release in the file
# will be used by default
release :triggerer do
set(version: current_version(:triggerer))
set(
applications: [
:runtime_tools
]
)
end

1
test/test_helper.exs Normal file
View file

@ -0,0 +1 @@
ExUnit.start()

4
test/triggerer_test.exs Normal file
View file

@ -0,0 +1,4 @@
defmodule TriggererTest do
use ExUnit.Case
doctest Triggerer
end