Current section
Files
Jump to
Current section
Files
lib/req_fly.ex
defmodule ReqFly do
@moduledoc """
A Req plugin for the Fly.io Machines API.
ReqFly provides a convenient interface to interact with Fly.io's Machines API
through the Req HTTP client library. It handles authentication, error handling,
retries, and telemetry automatically.
## Installation
Add ReqFly to your dependencies in `mix.exs`:
def deps do
[
{:req_fly, "~> 0.1.0"}
]
end
## Quick Start
Get your API token from https://fly.io/user/personal_access_tokens:
req = Req.new() |> ReqFly.attach(token: "your_fly_token")
# List apps
{:ok, apps} = ReqFly.Apps.list(req, org_slug: "personal")
# Create a machine
{:ok, machine} = ReqFly.Machines.create(req,
app_name: "my-app",
config: %{image: "nginx:latest"}
)
## High-Level APIs
ReqFly provides convenient helper modules for common operations:
* `ReqFly.Apps` - Create and manage Fly.io applications
* `ReqFly.Machines` - Full machine lifecycle management
* `ReqFly.Secrets` - Manage application secrets
* `ReqFly.Volumes` - Persistent storage operations
* `ReqFly.Orchestrator` - Multi-step workflows with polling
## Low-Level Plugin Usage
You can also use ReqFly as a Req plugin with direct HTTP calls:
req = Req.new() |> ReqFly.attach(token: "fly_token")
{:ok, %{body: apps}} = Req.get(req, url: "/apps", params: [org_slug: "personal"])
{:ok, %{body: app}} = Req.post(req, url: "/apps", json: %{app_name: "test", org_slug: "personal"})
## Configuration
### Explicit Token (Recommended)
req = Req.new() |> ReqFly.attach(token: "your_fly_token")
### Environment Variable
req = Req.new() |> ReqFly.attach(token: System.get_env("FLY_API_TOKEN"))
### Application Config
# config/config.exs
config :req_fly, token: "your_fly_token"
# In your code
req = Req.new() |> ReqFly.attach()
### Runtime Config (Recommended for Production)
# config/runtime.exs
config :req_fly, token: System.get_env("FLY_API_TOKEN")
## Options
* `:token` - Your Fly.io API token (required, can be set in app config)
* `:base_url` - Override the default API base URL (default: "https://api.machines.dev/v1")
* `:retry` - Retry strategy (default: `:safe_transient`)
* `:max_retries` - Maximum number of retries (default: 3)
* `:telemetry_prefix` - Custom telemetry event prefix (default: `[:req_fly]`)
## Authentication
Get your API token from: https://fly.io/user/personal_access_tokens
**Important:** Never commit your API token to version control. Use environment
variables or runtime configuration.
## Telemetry
ReqFly emits the following telemetry events:
* `[:req_fly, :request, :start]` - Emitted when a request starts
* `[:req_fly, :request, :stop]` - Emitted when a request completes successfully
* `[:req_fly, :request, :exception]` - Emitted when a request fails
You can attach handlers to monitor your API usage:
:telemetry.attach_many(
"req-fly-handler",
[
[:req_fly, :request, :start],
[:req_fly, :request, :stop],
[:req_fly, :request, :exception]
],
fn event_name, measurements, metadata, _config ->
# Handle telemetry event
Logger.info("Fly.io API call", event: event_name, metadata: metadata)
end,
nil
)
## Examples
# Basic usage with token
req = Req.new() |> ReqFly.attach(token: "fly_token")
{:ok, apps} = ReqFly.Apps.list(req, org_slug: "personal")
# Create app and machine
req = Req.new() |> ReqFly.attach(token: System.get_env("FLY_API_TOKEN"))
{:ok, app} = ReqFly.Apps.create(req, app_name: "my-app", org_slug: "personal")
config = %{
image: "nginx:latest",
guest: %{cpus: 1, memory_mb: 256}
}
{:ok, machine} = ReqFly.Machines.create(req, app_name: "my-app", config: config)
# Use orchestrator for complex workflows
{:ok, machine} = ReqFly.Orchestrator.create_machine_and_wait(req,
app_name: "my-app",
config: config,
timeout: 60
)
# Custom retry configuration
req = Req.new() |> ReqFly.attach(
token: "fly_token",
retry: :transient,
max_retries: 5
)
# Custom telemetry prefix
req = Req.new() |> ReqFly.attach(
token: "fly_token",
telemetry_prefix: [:my_app, :fly]
)
## Error Handling
All functions return `{:ok, result}` or `{:error, %ReqFly.Error{}}` tuples.
Use pattern matching to handle errors:
case ReqFly.Apps.get(req, "my-app") do
{:ok, app} ->
IO.puts("Found app: \#{app["name"]}")
{:error, %ReqFly.Error{status: 404}} ->
IO.puts("App not found")
{:error, %ReqFly.Error{status: status, message: message}} ->
IO.puts("Error \#{status}: \#{message}")
end
"""
alias ReqFly.Steps
@default_base_url "https://api.machines.dev/v1"
@doc """
Attaches the ReqFly plugin to a Req request.
## Options
* `:token` - Fly.io API token (required if not configured globally)
* `:base_url` - API base URL (default: "https://api.machines.dev/v1")
* `:retry` - Retry strategy (default: `:safe_transient`)
* `:max_retries` - Maximum retries (default: 3)
* `:telemetry_prefix` - Telemetry event prefix (default: `[:req_fly]`)
## Examples
req = Req.new() |> ReqFly.attach(token: "fly_token")
req = Req.new() |> ReqFly.attach(
token: "fly_token",
base_url: "https://custom.api.dev",
retry: :transient,
max_retries: 5
)
"""
@spec attach(Req.Request.t(), keyword()) :: Req.Request.t()
def attach(%Req.Request{} = request, options \\ []) do
token = options[:token] || get_configured_token()
if is_nil(token) do
raise ArgumentError, """
Fly.io API token is required. Provide it via:
1. ReqFly.attach(request, token: "your_token")
2. Application config: config :req_fly, token: "your_token"
"""
end
base_url = options[:base_url] || @default_base_url
retry_strategy = options[:retry] || :safe_transient
max_retries = options[:max_retries] || 3
telemetry_prefix = options[:telemetry_prefix] || [:req_fly]
request
|> Req.Request.register_options([
:fly_token,
:fly_base_url,
:fly_retry,
:fly_max_retries,
:fly_telemetry_prefix
])
|> Req.Request.merge_options(
fly_token: token,
fly_base_url: base_url,
fly_retry: retry_strategy,
fly_max_retries: max_retries,
fly_telemetry_prefix: telemetry_prefix,
decode_json: [keys: :strings]
)
|> Req.Request.prepend_request_steps([
{:fly_base_url, &Steps.attach_base_url/1},
{:fly_auth, &Steps.attach_auth/1},
{:fly_headers, &Steps.attach_headers/1}
])
|> Steps.attach_telemetry()
|> Req.Request.merge_options(
retry: retry_strategy,
max_retries: max_retries
)
end
@doc """
Helper function to make HTTP requests with the configured ReqFly client.
This is a convenience function that builds the full URL from base_url + path,
makes the HTTP request, and returns a simplified response tuple.
## Parameters
* `req` - A Req.Request with ReqFly attached
* `method` - HTTP method (`:get`, `:post`, `:put`, `:delete`, etc.)
* `path` - URL path (will be appended to base_url)
* `opts` - Additional options (`:params`, `:json`, etc.)
## Returns
* `{:ok, body}` - For successful 2xx responses
* `{:error, %ReqFly.Error{}}` - For errors
## Examples
req = Req.new() |> ReqFly.attach(token: "fly_token")
ReqFly.request(req, :get, "/apps")
# => {:ok, [%{"name" => "my-app"}]}
ReqFly.request(req, :post, "/apps/my-app/machines", json: %{config: %{}})
# => {:ok, %{"id" => "machine123"}}
ReqFly.request(req, :get, "/apps/nonexistent")
# => {:error, %ReqFly.Error{status: 404}}
"""
@spec request(Req.Request.t(), atom(), String.t(), keyword()) ::
{:ok, term()} | {:error, ReqFly.Error.t()}
def request(%Req.Request{} = req, method, path, opts \\ []) do
request_opts =
opts
|> Keyword.put(:url, path)
|> Keyword.put_new(:params, [])
case Req.request(req, [{:method, method} | request_opts]) do
{:ok, %Req.Response{status: status, body: body}} when status >= 200 and status < 300 ->
{:ok, body}
{:ok, %Req.Response{} = response} ->
{:error, ReqFly.Error.from_response(response, method: method, url: path)}
{:error, exception} ->
{:error, ReqFly.Error.from_exception(exception)}
end
end
# Private functions
@spec get_configured_token() :: String.t() | nil
defp get_configured_token do
Application.get_env(:req_fly, :token)
end
end