Initial commit
This commit is contained in:
commit
92767d41a2
15 changed files with 392 additions and 0 deletions
4
.formatter.exs
Normal file
4
.formatter.exs
Normal file
|
@ -0,0 +1,4 @@
|
|||
# Used by "mix format"
|
||||
[
|
||||
inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
||||
]
|
26
.gitignore
vendored
Normal file
26
.gitignore
vendored
Normal 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
6
.on-save.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
[
|
||||
{
|
||||
"files": "**/*.{ex,exs}",
|
||||
"command": "mix format ${srcFile}"
|
||||
}
|
||||
]
|
45
README.md
Normal file
45
README.md
Normal 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
34
config/config.exs
Normal 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
7
config/dev.exs
Normal 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
10
config/prod.exs
Normal 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
49
lib/router.ex
Normal 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
81
lib/triggerer.ex
Normal 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
|
30
lib/triggerer/application.ex
Normal file
30
lib/triggerer/application.ex
Normal 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
30
mix.exs
Normal 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
8
mix.lock
Normal 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
57
rel/config.exs
Normal 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
1
test/test_helper.exs
Normal file
|
@ -0,0 +1 @@
|
|||
ExUnit.start()
|
4
test/triggerer_test.exs
Normal file
4
test/triggerer_test.exs
Normal file
|
@ -0,0 +1,4 @@
|
|||
defmodule TriggererTest do
|
||||
use ExUnit.Case
|
||||
doctest Triggerer
|
||||
end
|
Loading…
Reference in a new issue