Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: shuttle-api-client #1833

Merged
merged 4 commits into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -872,6 +872,7 @@ workflows:
matrix:
parameters:
path:
- api-client
- proto
- service
name: publish-<< matrix.path >>
Expand All @@ -898,6 +899,7 @@ workflows:
- cargo-shuttle
name: publish-<< matrix.path >>
requires:
- publish-api-client
- publish-service
- publish-proto
- publish-crate:
Expand Down
30 changes: 24 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
resolver = "2"
members = [
"admin",
"api-client",
"auth",
"backends",
"cargo-shuttle",
Expand Down Expand Up @@ -31,6 +32,7 @@ repository = "https://github.com/shuttle-hq/shuttle"

# https://doc.rust-lang.org/cargo/reference/workspaces.html#the-workspacedependencies-table
[workspace.dependencies]
shuttle-api-client = { path = "api-client", version = "0.46.0" }
shuttle-backends = { path = "backends", version = "0.46.0" }
shuttle-codegen = { path = "codegen", version = "0.46.0" }
shuttle-common = { path = "common", version = "0.46.0" }
Expand Down Expand Up @@ -94,6 +96,9 @@ test-context = "0.3.0"
thiserror = "1.0.37"
tokio = "1.22.0"
tokio-stream = "0.1.11"
tokio-tungstenite = { version = "0.20.1", features = [
"rustls-tls-webpki-roots",
] }
tokio-util = "0.7.10"
toml = "0.8.2"
toml_edit = "0.20.2"
Expand Down
1 change: 1 addition & 0 deletions DEVELOPING.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ and renewing SSL certificates through the acme client in the `gateway`.

### Libraries

- `api-client` is a reqwest client for calling the backends.
- `common` contains shared models and functions used by the other libraries and binaries.
- `codegen` contains our proc-macro code which gets exposed to user services from `runtime`.
The redirect through `runtime` is to make it available under the prettier name of `shuttle_runtime::main`.
Expand Down
4 changes: 1 addition & 3 deletions admin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,13 @@ edition = "2021"
publish = false

[dependencies]
shuttle-api-client = { workspace = true }
shuttle-common = { workspace = true, features = ["models"] }
shuttle-backends = { workspace = true }

anyhow = { workspace = true }
bytes = { workspace = true }
clap = { workspace = true, features = ["env"] }
dirs = { workspace = true }
reqwest = { workspace = true, features = ["json"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
toml = { workspace = true }
Expand Down
11 changes: 9 additions & 2 deletions admin/src/args.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use std::{fs, io, path::PathBuf};

use clap::{Error, Parser, Subcommand};
use shuttle_common::models::user::UserId;
use shuttle_common::{constants::API_URL_PRODUCTION, models::user::UserId};

#[derive(Parser, Debug)]
pub struct Args {
/// run this command against the api at the supplied url
#[arg(long, default_value = "https://api.shuttle.rs", env = "SHUTTLE_API")]
#[arg(long, default_value = API_URL_PRODUCTION, env = "SHUTTLE_API")]
pub api_url: String,

#[command(subcommand)]
Expand Down Expand Up @@ -39,6 +39,13 @@ pub enum Command {

/// Forcefully idle CCH projects.
IdleCch,

SetBetaAccess {
user_id: String,
},
UnsetBetaAccess {
user_id: String,
},
}

#[derive(Subcommand, Debug)]
Expand Down
125 changes: 36 additions & 89 deletions admin/src/client.rs
Original file line number Diff line number Diff line change
@@ -1,34 +1,34 @@
use anyhow::{bail, Context, Result};
use bytes::Bytes;
use serde::{de::DeserializeOwned, Serialize};
use shuttle_common::models::{admin::ProjectResponse, stats, ToJson};
use tracing::trace;
use anyhow::Result;
use shuttle_api_client::ShuttleApiClient;
use shuttle_common::models::{admin::ProjectResponse, stats};

pub struct Client {
api_url: String,
api_key: String,
pub inner: ShuttleApiClient,
}

impl Client {
pub fn new(api_url: String, api_key: String) -> Self {
Self { api_url, api_key }
Self {
inner: ShuttleApiClient::new(api_url, Some(api_key), None),
}
}

pub async fn revive(&self) -> Result<String> {
self.post("/admin/revive", Option::<String>::None).await
self.inner
.post_json("/admin/revive", Option::<()>::None)
.await
}

pub async fn destroy(&self) -> Result<String> {
self.post("/admin/destroy", Option::<String>::None).await
self.inner
.post_json("/admin/destroy", Option::<()>::None)
.await
}

pub async fn idle_cch(&self) -> Result<()> {
reqwest::Client::new()
.post(format!("{}/admin/idle-cch", self.api_url))
.bearer_auth(&self.api_key)
.send()
.await
.context("failed to send idle request")?;
self.inner
.post("/admin/idle-cch", Option::<()>::None)
.await?;

Ok(())
}
Expand All @@ -39,7 +39,7 @@ impl Client {
acme_server: Option<String>,
) -> Result<serde_json::Value> {
let path = format!("/admin/acme/{email}");
self.post(&path, Some(acme_server)).await
self.inner.post_json(&path, Some(acme_server)).await
}

pub async fn acme_request_certificate(
Expand All @@ -49,7 +49,7 @@ impl Client {
credentials: &serde_json::Value,
) -> Result<String> {
let path = format!("/admin/acme/request/{project_name}/{fqdn}");
self.post(&path, Some(credentials)).await
self.inner.post_json(&path, Some(credentials)).await
}

pub async fn acme_renew_custom_domain_certificate(
Expand All @@ -59,101 +59,48 @@ impl Client {
credentials: &serde_json::Value,
) -> Result<String> {
let path = format!("/admin/acme/renew/{project_name}/{fqdn}");
self.post(&path, Some(credentials)).await
self.inner.post_json(&path, Some(credentials)).await
}

pub async fn acme_renew_gateway_certificate(
&self,
credentials: &serde_json::Value,
) -> Result<String> {
let path = "/admin/acme/gateway/renew".to_string();
self.post(&path, Some(credentials)).await
self.inner.post_json(&path, Some(credentials)).await
}

pub async fn get_projects(&self) -> Result<Vec<ProjectResponse>> {
self.get("/admin/projects").await
self.inner.get_json("/admin/projects").await
}

pub async fn change_project_owner(&self, project_name: &str, new_user_id: &str) -> Result<()> {
self.get_raw(&format!(
"/admin/projects/change-owner/{project_name}/{new_user_id}"
))
.await?;
self.inner
.get(format!(
"/admin/projects/change-owner/{project_name}/{new_user_id}"
))
.await?;

Ok(())
}

pub async fn get_load(&self) -> Result<stats::LoadResponse> {
self.get("/admin/stats/load").await
self.inner.get_json("/admin/stats/load").await
}

pub async fn clear_load(&self) -> Result<stats::LoadResponse> {
self.delete("/admin/stats/load", Option::<String>::None)
.await
}

async fn post<T: Serialize, R: DeserializeOwned>(
&self,
path: &str,
body: Option<T>,
) -> Result<R> {
trace!(self.api_key, "using api key");

let mut builder = reqwest::Client::new()
.post(format!("{}{}", self.api_url, path))
.bearer_auth(&self.api_key);

if let Some(body) = body {
builder = builder.json(&body);
}

builder
.send()
.await
.context("failed to make post request")?
.to_json()
.await
.context("failed to extract json body from post response")
self.inner.delete_json("/admin/stats/load").await
}

async fn delete<T: Serialize, R: DeserializeOwned>(
&self,
path: &str,
body: Option<T>,
) -> Result<R> {
trace!(self.api_key, "using api key");

let mut builder = reqwest::Client::new()
.delete(format!("{}{}", self.api_url, path))
.bearer_auth(&self.api_key);

if let Some(body) = body {
builder = builder.json(&body);
pub async fn set_beta_access(&self, user_id: &str, access: bool) -> Result<()> {
if access {
self.inner
.put(format!("/users/{user_id}/beta"), Option::<()>::None)
.await?;
} else {
self.inner.delete(format!("/users/{user_id}/beta")).await?;
}

builder
.send()
.await
.context("failed to make delete request")?
.to_json()
.await
.context("failed to extract json body from delete response")
}

async fn get_raw(&self, path: &str) -> Result<Bytes> {
let res = reqwest::Client::new()
.get(format!("{}{}", self.api_url, path))
.bearer_auth(&self.api_key)
.send()
.await
.context("making request")?;
if !res.status().is_success() {
bail!("API call returned non-2xx: {:?}", res);
}
res.bytes().await.context("getting response body")
}

async fn get<R: DeserializeOwned>(&self, path: &str) -> Result<R> {
serde_json::from_slice(&self.get_raw(path).await?).context("deserializing body")
Ok(())
}
}
11 changes: 9 additions & 2 deletions admin/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use clap::Parser;
use shuttle_admin::{
args::{AcmeCommand, Args, Command, StatsCommand},
client::Client,
Expand All @@ -11,7 +10,7 @@ use tracing::trace;
async fn main() {
tracing_subscriber::fmt::init();

let args = Args::parse();
let args: Args = clap::Parser::parse();

trace!(?args, "starting with args");

Expand Down Expand Up @@ -104,5 +103,13 @@ async fn main() {
.unwrap();
println!("Changed project owner: {project_name} -> {new_user_id}")
}
Command::SetBetaAccess { user_id } => {
client.set_beta_access(&user_id, true).await.unwrap();
println!("Set user {user_id} beta access");
}
Command::UnsetBetaAccess { user_id } => {
client.set_beta_access(&user_id, false).await.unwrap();
println!("Unset user {user_id} beta access");
}
};
}
Loading