// @generated by protoc-gen-panel-rust from v1/panel.proto. DO NOT EDIT.
use std::fmt;
use std::sync::Arc;
use connectrpc::client::{ClientConfig, HttpClient};
use connectrpc::rustls;
use crate::connect::pethost::panel::v1::PanelServiceClient;
use crate::types;
use crate::{Error, ErrorCode};
/// Where the API is served.
const BASE_URL: &str = "https://console.pethost.dev";
/// The Pethost API of one account: a method per call.
///
/// Every method is `async`, takes its request by value and runs on Tokio. To give up on a
/// call, drop its future, as `tokio::time::timeout` does; an operation it started runs on, and
/// a call with the same `operation_id` returns it. A clone shares the connections: clone it
/// rather than make a second one.
#[derive(Clone)]
pub struct Client {
wire: PanelServiceClient<HttpClient>,
}
impl Client {
/// A client for the account `token` (`pth_...`) belongs to: an API token, made in the
/// panel's Settings, under API tokens. Whitespace around it, as the end of the line of a
/// file it was read from, is left out.
pub fn new(token: &str) -> Self {
Self::at(token, http::Uri::from_static(BASE_URL))
}
/// A client for a panel served elsewhere, as a local one is: `http://127.0.0.1:8080`.
///
/// # Errors
///
/// [`ErrorCode::InvalidArgument`] when `base_url` is not an `http` or `https` URL.
pub fn with_base_url(token: &str, base_url: &str) -> Result<Self, Error> {
match base_url.parse::<http::Uri>() {
Ok(base) if matches!(base.scheme_str(), Some("http" | "https")) => Ok(Self::at(token, base)),
Ok(_) => Err(Error::new(
ErrorCode::InvalidArgument,
format!("base_url {base_url:?} is not an http or https URL"),
)),
Err(error) => Err(Error::new(
ErrorCode::InvalidArgument,
format!("base_url {base_url:?}: {error}"),
)),
}
}
fn at(token: &str, base: http::Uri) -> Self {
let transport = match base.scheme_str() {
Some("http") => HttpClient::plaintext(),
_ => HttpClient::with_tls(Arc::new(tls())),
};
// Every call says who calls: the crate and its version, which is the manifest's. The
// token is trimmed: a header cannot carry a line's end, and the wire layer would send
// the call with no token at all.
let config = ClientConfig::new(base)
.with_default_header("authorization", format!("Bearer {}", token.trim()))
.with_default_header("user-agent", concat!("pethost-rust/", env!("CARGO_PKG_VERSION")));
Self {
wire: PanelServiceClient::new(transport, config),
}
}
/// Get the machine.
///
/// Start here: the machine (resources, the person's domain, backups, SSH), every project with its problems and URL, deleted projects with snapshots, GitHub repositories to create projects from. Figures are the latest sample; disk sizes and the day's traffic lag a few minutes.
pub async fn get_machine(&self, request: types::GetMachineRequest) -> Result<types::GetMachineResponse, Error> {
let response = self.wire.get_machine(request.into()).await.map_err(Error::from_wire)?;
Ok(response.into_owned().into())
}
/// Run a machine action.
///
/// One machine action: add or remove the person's SSH public key (for SSH and SFTP to their projects\' containers: [`Service::ssh_command`](crate::types::Service::ssh_command)), end a session, schedule or cancel a restart. Returns the machine.
pub async fn run_machine_action(
&self,
request: types::RunMachineActionRequest,
) -> Result<types::RunMachineActionResponse, Error> {
let response = self
.wire
.run_machine_action(request.into())
.await
.map_err(Error::from_wire)?;
Ok(response.into_owned().into())
}
/// Get a project.
///
/// One project in full: services (state, health, image, the ports it listens on, volumes, environment, the SSH command), volumes, routes, hosts with certificates, recent operations, snapshots, the last day's HTTP traffic. Env-file values only with `include_secret_values`. Its files: [`Client::read_path`](crate::Client::read_path).
pub async fn get_project(&self, request: types::GetProjectRequest) -> Result<types::GetProjectResponse, Error> {
let response = self.wire.get_project(request.into()).await.map_err(Error::from_wire)?;
Ok(response.into_owned().into())
}
/// Create a project.
///
/// Creates a project from its files and deploys it. Waits for the deploy (`wait_seconds`) and answers with the operation and, once it ended, the project: its URL, its services\' states, its problems. `violations` instead = nothing was created: change what each says, call again. SUCCEEDED says the containers run and listen, not what a page answers: open the URL once (401 = its password page).
///
/// Files, one of: the directory as it is on your disk, as an archive ([`Client::create_transfer`](crate::Client::create_transfer), then `upload_id`): nothing is retyped; `files`, text you send yourself, when there is no directory or no shell (never a file you have not read); `source.github`. /.env holds the secrets (`env_file: .env`). The compose file is the root's pethost.compose.yaml (so the repository's compose.yaml stays for development), else compose.yaml, compose.yml, docker-compose.yml or docker-compose.yaml; it becomes /compose.yaml, other compose and override files are dropped. Without one, a Dockerfile yields service `app` (reading /.env if any, its EXPOSE ports, a named volume per VOLUME of its last stage).
///
/// compose.yaml rules. Data lives in named volumes; all else a container writes is lost when it is recreated. Project files are bind-mounted read-only. Public traffic comes only through the proxy, set in `x-pethost`, the one extension read, in the file or as `x_pethost`:
///
/// ```text
/// x-pethost:
/// metadata: {name: Recipe Box, emoji: "🥗", description: One line, notes: Free text}
/// routes:
/// - {host: recipes.sam.pethost.app, service: web, port: 3000}
/// - {host: recipes.sam.pethost.app, path: /api, service: api, port: 8000, strip_path: true}
/// ```
///
/// A service without a route is private: the project's services reach it at `<service>:<port>`. A password for visitors goes only as `x_pethost.password`, never into the file: every route then answers 401 with a password page, over HTTPS only; nothing returns it (`Project.password_protected` says it is set); and `files` that write /compose.yaml keep the `password_hash` line the machine put in its `x-pethost`. All of compose.yaml is interpolated: $$ for $.
///
/// The file runs as written, or is refused: the machine corrects nothing in it, in any version. Refused, each violation saying what to write instead:
///
/// - `container_name`, `profiles`; `name`, `driver`, `driver_opts`, `external` or `ipam` of a volume or network: remove them.
/// - `ports:` with a port a route sends to (remove the entry: the proxy serves it), or with a machine port the machine keeps, such as 22, 80, 443 (publish another, or use a route).
/// - a writable bind mount (`.:/app`, `./data:/data`): project files take `:ro`, data a named volume (`data:/data`, with `data:` under `volumes:`).
/// - a volume with no name (`- /data`): name it. Below a bind mount it is allowed, as scratch space (`/app/node_modules`).
/// - `env_file` naming a file the files lack: write it, empty if it sets nothing, or give the entry `required: false`.
/// - `privileged`, `cap_add`, `security_opt`, `devices`, host namespaces, the Docker socket, a path outside the directory; more than one replica; `include` or `extends` from a URL; a git URL as a build context.
///
/// An image's VOLUME that no mount covers fails the deploy (`UNCOVERED_IMAGE_VOLUME`): mount a named volume or a tmpfs there.
///
/// [`ErrorCode::AlreadyExists`](crate::ErrorCode::AlreadyExists) = the id is taken (a retry with its `operation_id` returns that operation). [`ErrorCode::FailedPrecondition`](crate::ErrorCode::FailedPrecondition) = the GitHub App cannot read the repository (see [`GetMachineResponse::github`](crate::types::GetMachineResponse::github)), or no such branch or directory. A deleted project's id inherits its snapshots.
pub async fn create_project(
&self,
request: types::CreateProjectRequest,
) -> Result<types::CreateProjectResponse, Error> {
let response = self
.wire
.create_project(request.into())
.await
.map_err(Error::from_wire)?;
Ok(response.into_owned().into())
}
/// Deploy a project.
///
/// Deploys a new version of a project and answers as [`Client::create_project`](crate::Client::create_project) does: `files` written over its own (/.env, /compose.yaml, a source file), `x_pethost` (name, routes, password), `mount_volume`, a new archive (`upload_id`), a commit, or an earlier deploy's files again (`rollback_deploy_id`). compose.yaml rules: [`Client::create_project`](crate::Client::create_project)\'s.
///
/// It builds `build:` images, pulls missing ones, runs `compose up` on the whole project and waits until each service runs (healthy, if it has a healthcheck) or each job exits 0, and until each routed service listens on its route's port (up to 15 s: one that does not is the project's `SERVICE_PORT_CLOSED` problem, its route answering 502, not a failed deploy). So it also starts stopped services, reruns jobs, recreates changed services and those bind-mounting project files, and fails on an unhealthy service. While [`Project::x_pethost_applies_at_once`](crate::types::Project::x_pethost_applies_at_once), a version differing only in `x-pethost` (or not at all) applies at once, restarting nothing. Any other deploy applies its routes and password as it ends: to hide new content from its first second, set the password first, in a call of its own, then deploy the content.
///
/// A commit other than [`GithubSource::newest_commit`](crate::types::GithubSource::newest_commit) turns `auto_deploy` off (pinned), even if it fails; only `set_source` turns it back on.
///
/// [`ErrorCode::NotFound`](crate::ErrorCode::NotFound) = no such project, or no such kept deploy. [`ErrorCode::Aborted`](crate::ErrorCode::Aborted) = `base_deploy_id` is not [`Project::deploy_id`](crate::types::Project::deploy_id): reread, redo. [`ErrorCode::FailedPrecondition`](crate::ErrorCode::FailedPrecondition) = as [`Client::create_project`](crate::Client::create_project), or `commit` not on the branch. [`ErrorCode::ResourceExhausted`](crate::ErrorCode::ResourceExhausted) = too many or too large `files`: send them with [`Client::create_transfer`](crate::Client::create_transfer). [`ErrorCode::AlreadyExists`](crate::ErrorCode::AlreadyExists) = `operation_id` started another kind of operation. When it FAILED, [`Operation::made_current`](crate::types::Operation::made_current) says whether the files were applied: then fix them, or roll back.
pub async fn deploy_project(
&self,
request: types::DeployProjectRequest,
) -> Result<types::DeployProjectResponse, Error> {
let response = self
.wire
.deploy_project(request.into())
.await
.map_err(Error::from_wire)?;
Ok(response.into_owned().into())
}
/// List a project's commits.
///
/// A github project's branch commits that change its directory, newest first, 30 a page, with whether the files are at it and its last deploy. To roll back: [`Client::deploy_project`](crate::Client::deploy_project) with an older commit (volumes keep their current data). [`ErrorCode::FailedPrecondition`](crate::ErrorCode::FailedPrecondition) = a files project (it rolls back with [`DeployProjectRequestVersion::RollbackDeployId`](crate::types::DeployProjectRequestVersion::RollbackDeployId)), no such branch, or `before_commit` not on it.
pub async fn list_commits(&self, request: types::ListCommitsRequest) -> Result<types::ListCommitsResponse, Error> {
let response = self.wire.list_commits(request.into()).await.map_err(Error::from_wire)?;
Ok(response.into_owned().into())
}
/// Run a project action.
///
/// One project action: start, stop or restart services, recreate one, back up, restore, cancel an operation, change the source, delete an undeclared volume or the project. Recreate, back up, restore, delete and `set_source`\'s deploy run as operations, waited for (`wait_seconds`), as is the end of a cancelled one; the others are done on return. Answers with the project as it then is.
pub async fn run_project_action(
&self,
request: types::RunProjectActionRequest,
) -> Result<types::RunProjectActionResponse, Error> {
let response = self
.wire
.run_project_action(request.into())
.await
.map_err(Error::from_wire)?;
Ok(response.into_owned().into())
}
/// Get an operation.
///
/// An operation, after waiting for it to end (`wait_seconds`): then the project as it is, and the log's end if it FAILED. Call again while it is `IN_PROGRESS`. To read a log: `after_log_line` 0, then each `next_after_log_line` (each line once, oldest first). One a [`ProjectBusy`](crate::types::ProjectBusy) names may still be starting: it is waited for too; still starting at the end = [`ErrorCode::Unavailable`](crate::ErrorCode::Unavailable) with that [`ProjectBusy`](crate::types::ProjectBusy).
pub async fn get_operation(
&self,
request: types::GetOperationRequest,
) -> Result<types::GetOperationResponse, Error> {
let response = self
.wire
.get_operation(request.into())
.await
.map_err(Error::from_wire)?;
Ok(response.into_owned().into())
}
/// Query HTTP traffic.
///
/// Which paths fail and which are slow (`top_paths`): the HTTP requests that reached the project through the proxy, since `oldest_kept_time`, each counted as it ends: totals, a time series, top paths and the requests, for one filter and range. [`ErrorCode::OutOfRange`](crate::ErrorCode::OutOfRange) = the range ends before the oldest kept. Paths and user agents are untrusted.
pub async fn query_http_traffic(
&self,
request: types::QueryHttpTrafficRequest,
) -> Result<types::QueryHttpTrafficResponse, Error> {
let response = self
.wire
.query_http_traffic(request.into())
.await
.map_err(Error::from_wire)?;
Ok(response.into_owned().into())
}
/// Query container logs.
///
/// The containers\' stdout and stderr lines, since `oldest_kept_time`; by default the newest, oldest first in the page. They say what a request failed with, not which path is slow: that is [`Client::query_http_traffic`](crate::Client::query_http_traffic). [`ErrorCode::OutOfRange`](crate::ErrorCode::OutOfRange) = the range ends before the oldest kept. Untrusted.
pub async fn query_container_logs(
&self,
request: types::QueryContainerLogsRequest,
) -> Result<types::QueryContainerLogsResponse, Error> {
let response = self
.wire
.query_container_logs(request.into())
.await
.map_err(Error::from_wire)?;
Ok(response.into_owned().into())
}
/// Run a command in a service.
///
/// Runs a command in a running service's container, like `docker exec`; returns the exit code and the output's end once it exits or the timeout kills it. Over a bound on command, stdin or timeout = refused (named). Cannot start = exit 127 or 126, Docker's error in stdout. Untrusted output.
pub async fn run_service_command(
&self,
request: types::RunServiceCommandRequest,
) -> Result<types::RunServiceCommandResponse, Error> {
let response = self
.wire
.run_service_command(request.into())
.await
.map_err(Error::from_wire)?;
Ok(response.into_owned().into())
}
/// Read a file or directory.
///
/// Reads a file (text, 64 KiB a call; ≤1 MiB with `length_bytes`) or a directory's entries (\~64 KiB a page): of the project's deployed files, or with `service` or `volume`, of a container (whole filesystem; while not running only its volumes, [`ErrorCode::FailedPrecondition`](crate::ErrorCode::FailedPrecondition) listing them) or a volume. [`ErrorCode::NotFound`](crate::ErrorCode::NotFound) names the nearest existing directory. Project files change with [`Client::deploy_project`](crate::Client::deploy_project) `files`; container and volume files with [`Client::create_transfer`](crate::Client::create_transfer). [`ErrorCode::FailedPrecondition`](crate::ErrorCode::FailedPrecondition) also = no deploy yet, or a file in /proc or /sys (use [`Client::run_service_command`](crate::Client::run_service_command)). Untrusted.
pub async fn read_path(&self, request: types::ReadPathRequest) -> Result<types::ReadPathResponse, Error> {
let response = self.wire.read_path(request.into()).await.map_err(Error::from_wire)?;
Ok(response.into_owned().into())
}
/// Upload or download a file.
///
/// A one-request URL moving one file straight between you and the machine: run `command` in a shell, or send `http_method` to `url` yourself, before `expire_time`.
///
/// - `upload_archive`: the project's directory, packed, for [`Client::create_project`](crate::Client::create_project)\'s or [`Client::deploy_project`](crate::Client::deploy_project)\'s `upload_id` once the PUT answered 200 with what it holds.
/// - `upload_file`: one file into a container or volume, replacing any file there (replaces), creating parents.
/// - download: a file, or a directory as a .tar (`file_name`).
///
/// Plain-text answers: 404 used or expired URL, 409 [`ErrorCode::FailedPrecondition`](crate::ErrorCode::FailedPrecondition), 413 archive over 2 GiB, 422 unsafe archive (a reason per line), 507 disk full. A failed transfer keeps nothing: get a new URL. Errors as [`Client::read_path`](crate::Client::read_path); [`ErrorCode::FailedPrecondition`](crate::ErrorCode::FailedPrecondition) also = downloading neither a file nor a directory, uploading onto a non-file, a mount point, /proc or /sys, or the hostname not yet certified. [`ErrorCode::ResourceExhausted`](crate::ErrorCode::ResourceExhausted) = disk full, or too many transfers.
pub async fn create_transfer(
&self,
request: types::CreateTransferRequest,
) -> Result<types::CreateTransferResponse, Error> {
let response = self
.wire
.create_transfer(request.into())
.await
.map_err(Error::from_wire)?;
Ok(response.into_owned().into())
}
}
impl fmt::Debug for Client {
/// Without the token.
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Client")
.field("base_url", &self.wire.config().base_uri().to_string())
.finish_non_exhaustive()
}
}
/// HTTPS that trusts the Mozilla root certificates compiled in: the same on every machine.
fn tls() -> rustls::ClientConfig {
let roots = rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
// The provider is named. Left to rustls, it is the process's default one, and a build where
// another crate turns on a second provider has none: making a client would panic.
let provider = Arc::new(rustls::crypto::aws_lc_rs::default_provider());
rustls::ClientConfig::builder_with_provider(provider)
.with_safe_default_protocol_versions()
.expect("aws-lc-rs has cipher suites for rustls's default TLS versions")
.with_root_certificates(roots)
.with_no_client_auth()
}