# ReqFly
Req plugin for the Fly.io Machines API.
[](https://hex.pm/packages/req_fly)
[](https://hexdocs.pm/req_fly)
## Installation
Add `req_fly` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:req_fly, "~> 0.1.0"}
]
end
```
## Quick Start
Get your API token from https://fly.io/user/personal_access_tokens, then:
```elixir
# Create a Req client with ReqFly attached
req = Req.new() |> ReqFly.attach(token: "your_fly_token")
# List apps
{:ok, apps} = ReqFly.Apps.list(req, org_slug: "personal")
# Create an app
{:ok, app} = ReqFly.Apps.create(req,
app_name: "my-app",
org_slug: "personal"
)
# Create a machine
{:ok, machine} = ReqFly.Machines.create(req,
app_name: "my-app",
config: %{
image: "nginx:latest",
env: %{"PORT" => "8080"},
guest: %{cpus: 1, memory_mb: 256}
},
region: "ewr"
)
```
## Features
- ✅ **Full Fly.io Machines API coverage** - Apps, Machines, Secrets, Volumes, and more
- ✅ **Built-in retries and error handling** - Automatic retry with exponential backoff
- ✅ **Comprehensive documentation** - Every function documented with examples
- ✅ **High-level convenience functions** - Simplified APIs for common workflows
- ✅ **Telemetry support** - Built-in observability for all operations
- ✅ **Well-tested** - 187 tests with real API fixtures using ExVCR
## API Overview
### Low-Level Plugin Usage
ReqFly is a Req plugin that can be used with direct Req calls:
```elixir
req = Req.new() |> ReqFly.attach(token: "your_fly_token")
# Make direct API calls
{: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"})
{:ok, %{body: machines}} = Req.get(req, url: "/apps/my-app/machines")
```
### High-Level Helper APIs
For convenience, ReqFly provides high-level helper modules:
#### Apps - Create and manage apps
```elixir
ReqFly.Apps.list(req, org_slug: "personal")
ReqFly.Apps.create(req, app_name: "my-app", org_slug: "personal")
ReqFly.Apps.get(req, "my-app")
ReqFly.Apps.destroy(req, "my-app")
```
#### Machines - Full lifecycle management
```elixir
ReqFly.Machines.list(req, app_name: "my-app")
ReqFly.Machines.create(req, app_name: "my-app", config: %{image: "nginx:latest"})
ReqFly.Machines.start(req, app_name: "my-app", machine_id: "148ed...")
ReqFly.Machines.stop(req, app_name: "my-app", machine_id: "148ed...")
ReqFly.Machines.restart(req, app_name: "my-app", machine_id: "148ed...")
ReqFly.Machines.wait(req, app_name: "my-app", machine_id: "148ed...", state: "started")
```
#### Secrets - Manage app secrets
```elixir
ReqFly.Secrets.list(req, app_name: "my-app")
ReqFly.Secrets.create(req, app_name: "my-app", label: "DATABASE_URL", type: "env", value: "postgres://...")
ReqFly.Secrets.generate(req, app_name: "my-app", label: "SECRET_KEY", type: "env")
ReqFly.Secrets.destroy(req, app_name: "my-app", label: "OLD_SECRET")
```
#### Volumes - Persistent storage
```elixir
ReqFly.Volumes.list(req, app_name: "my-app")
ReqFly.Volumes.create(req, app_name: "my-app", name: "data", region: "ewr", size_gb: 10)
ReqFly.Volumes.extend(req, app_name: "my-app", volume_id: "vol_...", size_gb: 20)
ReqFly.Volumes.create_snapshot(req, app_name: "my-app", volume_id: "vol_...")
```
#### Orchestrator - Multi-step workflows
```elixir
# Create app and wait for it to become active
{:ok, app} = ReqFly.Orchestrator.create_app_and_wait(req,
app_name: "my-app",
org_slug: "personal",
timeout: 120
)
# Create machine and wait for it to start
{:ok, machine} = ReqFly.Orchestrator.create_machine_and_wait(req,
app_name: "my-app",
config: %{image: "nginx:latest"},
timeout: 60
)
```
## Configuration
### Explicit Token (Recommended)
```elixir
req = Req.new() |> ReqFly.attach(token: "your_fly_token")
```
### Environment Variable
```elixir
req = Req.new() |> ReqFly.attach(token: System.get_env("FLY_API_TOKEN"))
```
### Application Config
```elixir
# config/config.exs
config :req_fly, token: "your_fly_token"
# In your code
req = Req.new() |> ReqFly.attach()
```
### Runtime Config
```elixir
# config/runtime.exs
config :req_fly, token: System.get_env("FLY_API_TOKEN")
```
### All Options
```elixir
req = Req.new() |> ReqFly.attach(
token: "your_fly_token", # API token (required)
base_url: "https://...", # Override base URL (optional)
retry: :safe_transient, # Retry strategy (default: :safe_transient)
max_retries: 3, # Max retry attempts (default: 3)
telemetry_prefix: [:req_fly] # Telemetry event prefix (default: [:req_fly])
)
```
## Usage Examples
### Apps Management
```elixir
req = Req.new() |> ReqFly.attach(token: System.get_env("FLY_API_TOKEN"))
# List all apps in your organization
{:ok, apps} = ReqFly.Apps.list(req, org_slug: "personal")
IO.inspect(apps, label: "Apps")
# Create a new app
{:ok, app} = ReqFly.Apps.create(req,
app_name: "my-new-app",
org_slug: "personal"
)
# Get app details
{:ok, app} = ReqFly.Apps.get(req, "my-new-app")
# Clean up - destroy app
{:ok, _} = ReqFly.Apps.destroy(req, "my-new-app")
```
### Machine Lifecycle
```elixir
req = Req.new() |> ReqFly.attach(token: System.get_env("FLY_API_TOKEN"))
# Create a machine
config = %{
image: "flyio/hellofly:latest",
env: %{
"PORT" => "8080",
"ENV" => "production"
},
guest: %{
cpus: 1,
memory_mb: 256
},
services: [
%{
ports: [
%{port: 80, handlers: ["http"]},
%{port: 443, handlers: ["tls", "http"]}
],
protocol: "tcp",
internal_port: 8080
}
]
}
{:ok, machine} = ReqFly.Machines.create(req,
app_name: "my-app",
config: config,
region: "ewr"
)
machine_id = machine["id"]
# Start the machine
{:ok, _} = ReqFly.Machines.start(req, app_name: "my-app", machine_id: machine_id)
# Wait for it to reach started state
{:ok, ready_machine} = ReqFly.Machines.wait(req,
app_name: "my-app",
machine_id: machine_id,
instance_id: machine["instance_id"],
state: "started",
timeout: 60
)
# Stop the machine
{:ok, _} = ReqFly.Machines.stop(req, app_name: "my-app", machine_id: machine_id)
# Destroy the machine
{:ok, _} = ReqFly.Machines.destroy(req, app_name: "my-app", machine_id: machine_id)
```
### Secrets Management
```elixir
req = Req.new() |> ReqFly.attach(token: System.get_env("FLY_API_TOKEN"))
# Set a secret
{:ok, _} = ReqFly.Secrets.create(req,
app_name: "my-app",
label: "DATABASE_URL",
type: "env",
value: "postgres://user:pass@host/db"
)
# Generate a random secret
{:ok, secret} = ReqFly.Secrets.generate(req,
app_name: "my-app",
label: "SECRET_KEY_BASE",
type: "env"
)
IO.puts("Generated secret: #{secret["value"]}")
# List all secrets
{:ok, secrets} = ReqFly.Secrets.list(req, app_name: "my-app")
# Delete a secret
{:ok, _} = ReqFly.Secrets.destroy(req, app_name: "my-app", label: "OLD_SECRET")
```
### Volume Management
```elixir
req = Req.new() |> ReqFly.attach(token: System.get_env("FLY_API_TOKEN"))
# Create a volume
{:ok, volume} = ReqFly.Volumes.create(req,
app_name: "my-app",
name: "postgres_data",
region: "ewr",
size_gb: 10
)
volume_id = volume["id"]
# Extend volume size
{:ok, extended_volume} = ReqFly.Volumes.extend(req,
app_name: "my-app",
volume_id: volume_id,
size_gb: 20
)
# Create a snapshot
{:ok, snapshot} = ReqFly.Volumes.create_snapshot(req,
app_name: "my-app",
volume_id: volume_id
)
# List all snapshots
{:ok, snapshots} = ReqFly.Volumes.list_snapshots(req,
app_name: "my-app",
volume_id: volume_id
)
```
### Orchestrator Workflows
```elixir
req = Req.new() |> ReqFly.attach(token: System.get_env("FLY_API_TOKEN"))
# Create app and wait for it to be ready
{:ok, app} = ReqFly.Orchestrator.create_app_and_wait(req,
app_name: "production-app",
org_slug: "personal",
timeout: 120
)
# Create machine and wait for it to start
config = %{
image: "nginx:latest",
guest: %{cpus: 1, memory_mb: 256}
}
{:ok, machine} = ReqFly.Orchestrator.create_machine_and_wait(req,
app_name: "production-app",
config: config,
region: "ewr",
timeout: 60
)
IO.puts("Machine #{machine["id"]} is ready!")
```
### Error Handling
```elixir
req = Req.new() |> ReqFly.attach(token: System.get_env("FLY_API_TOKEN"))
case ReqFly.Apps.get(req, "nonexistent-app") do
{:ok, app} ->
IO.puts("Found app: #{app["name"]}")
{:error, %ReqFly.Error{status: 404}} ->
IO.puts("App not found")
{:error, %ReqFly.Error{status: 401}} ->
IO.puts("Authentication failed - check your token")
{:error, %ReqFly.Error{status: status, message: message}} ->
IO.puts("Error #{status}: #{message}")
end
```
### Telemetry
```elixir
# Attach telemetry handler
:telemetry.attach_many(
"req-fly-handler",
[
[:req_fly, :request, :start],
[:req_fly, :request, :stop],
[:req_fly, :request, :exception]
],
fn event_name, measurements, metadata, _config ->
IO.inspect({event_name, measurements, metadata})
end,
nil
)
# Make requests and observe telemetry events
req = Req.new() |> ReqFly.attach(token: System.get_env("FLY_API_TOKEN"))
ReqFly.Apps.list(req, org_slug: "personal")
```
## Resources
- **[Full API Documentation](https://hexdocs.pm/req_fly)** - Complete module and function documentation
- **[Fly.io Machines API Documentation](https://fly.io/docs/machines/api/)** - Official Fly.io API docs
- **[Req Documentation](https://hexdocs.pm/req)** - Learn about the Req HTTP client
### Module Documentation
- [ReqFly](https://hexdocs.pm/req_fly/ReqFly.html) - Main plugin module
- [ReqFly.Apps](https://hexdocs.pm/req_fly/ReqFly.Apps.html) - Apps API
- [ReqFly.Machines](https://hexdocs.pm/req_fly/ReqFly.Machines.html) - Machines API
- [ReqFly.Secrets](https://hexdocs.pm/req_fly/ReqFly.Secrets.html) - Secrets API
- [ReqFly.Volumes](https://hexdocs.pm/req_fly/ReqFly.Volumes.html) - Volumes API
- [ReqFly.Orchestrator](https://hexdocs.pm/req_fly/ReqFly.Orchestrator.html) - Multi-step workflows
## Development
### Running Tests
```bash
# Run all tests
mix test
# Run with coverage
mix test --cover
# Run with ExVCR cassettes
FLY_API_TOKEN=your_token mix test
```
### Building Documentation
```bash
# Generate documentation
mix docs
# Open documentation in browser
open doc/index.html
```
### Code Quality
```bash
# Run static analysis
mix credo
# Run type checking
mix dialyzer
# Format code
mix format
```
## License
MIT License - see [LICENSE](LICENSE) for details.
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Write tests for your changes
4. Ensure all tests pass (`mix test`)
5. Commit your changes (`git commit -am 'Add amazing feature'`)
6. Push to the branch (`git push origin feature/amazing-feature`)
7. Open a Pull Request
## Acknowledgments
Built with [Req](https://github.com/wojtekmach/req) - the awesome Elixir HTTP client.