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