use std::net::Ipv6Addr;
use http::header;
use reqwest::{IntoUrl, Method, RequestBuilder, Url};
use serde::{Deserialize, Serialize};
#[cfg(feature = "environment")]
use crate::placement::{private_address, Placement};
#[cfg(feature = "regions")]
use crate::Region;
use crate::{Error, Location};
pub struct Client {
http: reqwest::Client,
origin: Url,
token: String,
}
impl Client {
pub const PUBLIC_ORIGIN: &'static str = "https://api.machines.dev";
pub const PRIVATE_ORIGIN: &'static str = "http://_api.internal:4280";
pub const USER_AGENT: &'static str =
concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
#[cfg(feature = "environment")]
#[cfg_attr(docsrs, doc(cfg(feature = "environment")))]
pub fn new(token: impl Into<String>) -> Self {
Self::with_origin(Self::default_origin(), token)
}
pub fn with_origin(origin: impl IntoUrl, token: impl Into<String>) -> Self {
Self::with_client(Default::default(), origin, token)
}
pub fn with_client(
http_client: reqwest::Client,
origin: impl IntoUrl,
token: impl Into<String>,
) -> Self {
Self {
http: http_client,
origin: origin
.into_url()
.expect("invalid Fly.io Machines API base URL"),
token: token.into(),
}
}
pub async fn apps(&self, organization: impl AsRef<str>) -> Result<OrganizationApps, Error> {
self.request(Method::GET, "/v1/apps")
.query(&OrganizationAppsQuery {
organization: organization.as_ref(),
})
.send()
.await
.map_err(Error::from)?
.error_for_status()
.map_err(Error::from)?
.json()
.await
.map_err(Error::from)
}
pub async fn machines(&self, app: impl AsRef<str>) -> Result<Vec<Machine>, Error> {
let app = app.as_ref();
self.request(Method::GET, format!("/v1/apps/{app}/machines"))
.send()
.await
.map_err(Error::from)?
.error_for_status()
.map_err(Error::from)?
.json()
.await
.map_err(Error::from)
}
#[cfg(feature = "environment")]
#[cfg_attr(docsrs, doc(cfg(feature = "environment")))]
pub async fn peers(&self) -> Result<Vec<Machine>, Error> {
let placement = Placement::current()?;
let id = match placement.machine {
Some(ref machine) => machine.id.as_str(),
None => return Err(Error::Unavailable),
};
let mut machines = self.machines(&placement.app).await?;
machines.retain(move |m| m.id != id);
Ok(machines)
}
fn request(&self, method: Method, url: impl AsRef<str>) -> RequestBuilder {
let url = self
.origin
.join(url.as_ref())
.expect("invalid Machines API request URL");
self.http
.request(method, url)
.header(
header::AUTHORIZATION,
format!("Bearer {}", self.token.as_str()),
)
.header(header::USER_AGENT, Self::USER_AGENT)
}
#[cfg(feature = "environment")]
fn default_origin() -> Url {
let origin = match private_address() {
Some(_) => Self::PRIVATE_ORIGIN,
None => Self::PUBLIC_ORIGIN,
};
Url::parse(origin).unwrap()
}
}
#[cfg(feature = "environment")]
#[cfg_attr(docsrs, doc(cfg(feature = "environment")))]
impl Default for Client {
fn default() -> Self {
let token = std::env::var("FLY_API_TOKEN").expect("$FLY_API_TOKEN not set");
Self {
http: Default::default(),
origin: Self::default_origin(),
token,
}
}
}
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct OrganizationApps {
#[serde(rename = "total_apps")]
pub total: usize,
pub apps: Vec<AppEntry>,
}
impl OrganizationApps {
#[inline]
pub fn iter(&self) -> std::slice::Iter<'_, AppEntry> {
self.apps.iter()
}
}
impl IntoIterator for OrganizationApps {
type Item = AppEntry;
type IntoIter = std::vec::IntoIter<AppEntry>;
fn into_iter(self) -> Self::IntoIter {
self.apps.into_iter()
}
}
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct AppEntry {
pub id: String,
pub name: String,
pub machine_count: usize,
#[serde(rename = "network")]
pub network_name: String,
}
#[derive(Serialize, Debug)]
struct OrganizationAppsQuery<'a> {
#[serde(rename = "org_slug")]
pub organization: &'a str,
}
#[derive(Deserialize, Serialize, Clone, Debug)]
#[non_exhaustive]
pub struct Machine {
pub id: String,
pub name: String,
pub state: MachineState,
#[serde(rename = "region")]
pub location: Location,
pub instance_id: String,
pub private_ip: Ipv6Addr,
#[serde(default)]
pub checks: Vec<MachineCheckState>,
#[serde(default)]
pub host_status: HostStatus,
}
impl Machine {
pub fn is_running(&self) -> bool {
self.state.is_ready()
}
pub fn is_ready(&self) -> bool {
self.is_running()
&& self.host_status.is_ready()
&& self.checks.iter().all(MachineCheckState::is_ready)
}
#[cfg(feature = "regions")]
#[cfg_attr(docsrs, doc(cfg(feature = "regions")))]
pub const fn region(&self) -> Option<Region> {
self.location.region()
}
}
#[derive(Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Debug)]
#[serde(rename_all = "kebab-case")]
pub enum MachineState {
Created,
Starting,
Started,
Stopping,
Stopped,
Replacing,
Destroying,
Destroyed,
}
impl MachineState {
#[inline]
pub const fn is_ready(&self) -> bool {
matches!(self, Self::Started)
}
pub const fn target(&self) -> Option<Self> {
match self {
Self::Starting => Some(Self::Started),
Self::Stopping => Some(Self::Stopped),
Self::Replacing => Some(Self::Stopped), Self::Destroying => Some(Self::Destroyed),
_ => None,
}
}
#[inline]
pub const fn is_transition(&self) -> bool {
self.target().is_some()
}
}
#[derive(Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Default, Debug)]
#[serde(rename_all = "kebab-case")]
pub enum HostStatus {
#[default]
Ok,
Unreachable,
Unknown,
}
impl HostStatus {
#[inline]
pub const fn is_ready(&self) -> bool {
matches!(self, Self::Ok)
}
}
#[derive(Deserialize, Serialize, PartialEq, Eq, Clone, Debug)]
pub struct MachineCheckState {
pub name: String,
pub status: CheckStatus,
pub output: Option<String>,
}
impl MachineCheckState {
#[inline]
pub const fn is_ready(&self) -> bool {
self.status.is_ready()
}
}
#[derive(Deserialize, Serialize, PartialEq, Eq, Copy, Clone, Debug)]
#[serde(rename_all = "kebab-case")]
pub enum CheckStatus {
Passing,
Warning,
Critical,
}
impl CheckStatus {
#[inline]
pub const fn is_ready(&self) -> bool {
matches!(self, Self::Passing)
}
}