Skip to content

Commit

Permalink
feat: add a custom domains admin route (#473)
Browse files Browse the repository at this point in the history
Co-authored-by: Alex Krantz <[email protected]>
  • Loading branch information
chesedo and akrantz01 authored Nov 16, 2022
1 parent e10f096 commit 7b80c45
Show file tree
Hide file tree
Showing 20 changed files with 780 additions and 221 deletions.
90 changes: 89 additions & 1 deletion Cargo.lock

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

8 changes: 8 additions & 0 deletions admin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,13 @@ anyhow = "1.0.62"
clap = { version = "4.0.0", features = [ "derive", "env" ] }
dirs = "4.0.0"
reqwest = { version = "0.11.11", features = ["json"] }
serde = { version = "1.0.143", features = ["derive"] }
serde_json = "1.0.83"
tokio = { version = "1.20.1", features = ["macros", "rt-multi-thread"] }
toml = "0.5.9"
tracing = "0.1.35"
tracing-subscriber = { version = "0.3.11", features = ["env-filter"] }

[dependencies.shuttle-common]
version = "0.7.2"
path = "../common"
34 changes: 34 additions & 0 deletions admin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
_Small utility used by the shuttle admin for common tasks_

## How to test custom domain certificates locally
For local testing it is easiest to use the [Pebble](https://github.com/letsencrypt/pebble) server. So install it using
whatever method works for your system. It is included in the nix environment if you use it though.

To start the `Pebble` server you'll need some config, a root CA and a certificate signed with the CA. The easiest way
to get all these is to get them from the [pebble/test](https://github.com/letsencrypt/pebble/tree/main/test) folder.

You should now be able to start `Pebble` locally. If you used the `pebble/test` folder, then your important
variables are as follow:

- *Server url*: `https://localhost:14000/dir`
- *CA location*: `$PWD/test/certs/pebble.minica.pem`

Next you'll need `gateway` to use this CA when checking the TLS connection with localhost. This can be done by
setting the `SSL_CERT_FILE` environment variable.

``` shell
export SSL_CERT_FILE="$PWD/test/certs/pebble.minica.pem"
```

When `gateway` now runs, it will use this root certificate to check the certificate presented by `Pebble`.

Now you'll want this admin client to use the local `Pebble` server when making new account. Therefore, use the
following command when you create new accounts

``` shell
cargo run -p shuttle-admin -- --api-url http://localhost:8001 acme create-account --acme-server https://localhost:14000/dir --email <email>
```

Safe the account JSON in a local file and use it to test creating new certificate. However, you'll the FQDN you're
using for testnig to resolve to your local machine. So create an `A` record for it on your DNS with the value
`127.0.0.1`. And Bob's your uncle 🎉
36 changes: 36 additions & 0 deletions admin/src/args.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use std::path::PathBuf;

use clap::{Parser, Subcommand};
use shuttle_common::project::ProjectName;

#[derive(Parser, Debug)]
pub struct Args {
Expand All @@ -14,4 +17,37 @@ pub struct Args {
pub enum Command {
/// Try to revive projects in the crashed state
Revive,

#[command(subcommand)]
Acme(AcmeCommand),
}

#[derive(Subcommand, Debug)]
pub enum AcmeCommand {
/// Create a new ACME account. Should only be needed once
CreateAccount {
/// Email for managing all certificates
#[arg(long)]
email: String,

/// Acme server to create account on. Gateway will default to LetsEncrypt
#[arg(long)]
acme_server: Option<String>,
},

/// Request a certificate for a FQDN
RequestCertificate {
/// Fqdn to request certificate for
#[arg(long)]
fqdn: String,

/// Project to request certificate for
#[arg(long)]
project: ProjectName,

/// Path to acme credentials file
/// This should have been created with `acme create-account`
#[arg(long)]
credentials: PathBuf,
},
}
46 changes: 40 additions & 6 deletions admin/src/client.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use anyhow::{Context, Result};
use serde::{de::DeserializeOwned, Serialize};
use shuttle_common::{models::ToJson, project::ProjectName};
use tracing::trace;

pub struct Client {
api_url: String,
Expand All @@ -11,18 +14,49 @@ impl Client {
}

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

async fn post(&self, path: &str) -> Result<String> {
reqwest::Client::new()
pub async fn acme_account_create(
&self,
email: &str,
acme_server: Option<String>,
) -> Result<serde_json::Value> {
let path = format!("/admin/acme/{email}");
self.post(&path, Some(acme_server)).await
}

pub async fn acme_request_certificate(
&self,
fqdn: &str,
project_name: &ProjectName,
credentials: &serde_json::Value,
) -> Result<String> {
let path = format!("/admin/acme/request/{project_name}/{fqdn}");
self.post(&path, Some(credentials)).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)
.bearer_auth(&self.api_key);

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

builder
.send()
.await
.context("failed to make post request")?
.text()
.to_json()
.await
.context("failed to post text body from response")
.context("failed to extract json body from post response")
}
}
5 changes: 4 additions & 1 deletion admin/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ pub fn get_api_key() -> String {
let data = fs::read_to_string(config_path()).expect("shuttle config file to exist");
let toml: toml::Value = toml::from_str(&data).expect("to parse shuttle config file");

toml["api_key"].to_string()
toml["api_key"]
.as_str()
.expect("api key to be a string")
.to_string()
}

fn config_path() -> PathBuf {
Expand Down
Loading

0 comments on commit 7b80c45

Please sign in to comment.