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(cargo-shuttle): beta certificate command #1860

Merged
merged 11 commits into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
8 changes: 6 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,13 @@ DOCKER_COMPOSE_ENV=\
DOCKER_SOCK=$(DOCKER_SOCK)\
SHUTTLE_ENV=$(SHUTTLE_ENV)\
SHUTTLE_SERVICE_VERSION=$(SHUTTLE_SERVICE_VERSION)\
PERMIT_API_KEY=$(PERMIT_API_KEY)
PERMIT_API_KEY=$(PERMIT_API_KEY)\
PERMIT_DEV_API_KEY=$(PERMIT_DEV_API_KEY)

.PHONY: clean deep-clean images the-shuttle-images shuttle-% postgres otel deploy test docker-compose.rendered.yml up down
.PHONY: envfile clean deep-clean images the-shuttle-images shuttle-% postgres otel deploy test docker-compose.rendered.yml up down

envfile:
echo $(DOCKER_COMPOSE_ENV) > dockerenv

clean:
rm .shuttle-*
Expand Down
20 changes: 14 additions & 6 deletions admin/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,10 @@ impl Client {

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

Ok(())
Expand All @@ -93,12 +94,19 @@ impl Client {
}

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

if !resp.status().is_success() {
dbg!(resp);
panic!("request failed");
}

Ok(())
Expand Down
69 changes: 64 additions & 5 deletions api-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ use reqwest::Response;
use reqwest_middleware::{ClientWithMiddleware, RequestBuilder};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use shuttle_common::certificate::{AddCertificateRequest, CertificateResponse};
use shuttle_common::certificate::{
AddCertificateRequest, CertificateResponse, DeleteCertificateRequest,
};
use shuttle_common::log::{LogsRange, LogsResponseBeta};
use shuttle_common::models::deployment::{
DeploymentRequest, DeploymentRequestBeta, UploadArchiveResponseBeta,
Expand Down Expand Up @@ -228,6 +230,10 @@ impl ShuttleApiClient {
.await
}

pub async fn list_certificates_beta(&self, project: &str) -> Result<Vec<CertificateResponse>> {
self.get_json(format!("/projects/{project}/certificates"))
.await
}
pub async fn add_certificate_beta(
&self,
project: &str,
Expand All @@ -239,6 +245,13 @@ impl ShuttleApiClient {
)
.await
}
pub async fn delete_certificate_beta(&self, project: &str, domain: String) -> Result<()> {
self.delete_json_with_body(
format!("/projects/{project}/certificates"),
DeleteCertificateRequest { domain },
)
.await
}

pub async fn create_project(
&self,
Expand Down Expand Up @@ -450,20 +463,43 @@ impl ShuttleApiClient {
Ok(stream)
}

pub async fn get(&self, path: impl AsRef<str>) -> Result<Response> {
pub async fn get<T: Serialize>(
&self,
path: impl AsRef<str>,
body: Option<T>,
) -> Result<Response> {
let url = format!("{}{}", self.api_url, path.as_ref());

let mut builder = self.client.get(url);
builder = self.set_auth_bearer(builder);

if let Some(body) = body {
let body = serde_json::to_string(&body)?;
#[cfg(feature = "tracing")]
debug!("Outgoing body: {}", body);
builder = builder.body(body);
builder = builder.header("Content-Type", "application/json");
}

builder.send().await.context("failed to make get request")
}

pub async fn get_json<R>(&self, path: impl AsRef<str>) -> Result<R>
where
R: for<'de> Deserialize<'de>,
{
self.get(path).await?.to_json().await
self.get(path, Option::<()>::None).await?.to_json().await
}

pub async fn get_json_with_body<R, T: Serialize>(
&self,
path: impl AsRef<str>,
body: T,
) -> Result<R>
where
R: for<'de> Deserialize<'de>,
{
self.get(path, Some(body)).await?.to_json().await
}

pub async fn post<T: Serialize>(
Expand Down Expand Up @@ -530,12 +566,24 @@ impl ShuttleApiClient {
self.put(path, body).await?.to_json().await
}

pub async fn delete(&self, path: impl AsRef<str>) -> Result<Response> {
pub async fn delete<T: Serialize>(
&self,
path: impl AsRef<str>,
body: Option<T>,
) -> Result<Response> {
let url = format!("{}{}", self.api_url, path.as_ref());

let mut builder = self.client.delete(url);
builder = self.set_auth_bearer(builder);

if let Some(body) = body {
let body = serde_json::to_string(&body)?;
#[cfg(feature = "tracing")]
debug!("Outgoing body: {}", body);
builder = builder.body(body);
builder = builder.header("Content-Type", "application/json");
}

builder
.send()
.await
Expand All @@ -546,6 +594,17 @@ impl ShuttleApiClient {
where
R: for<'de> Deserialize<'de>,
{
self.delete(path).await?.to_json().await
self.delete(path, Option::<()>::None).await?.to_json().await
}

pub async fn delete_json_with_body<R, T: Serialize>(
&self,
path: impl AsRef<str>,
body: T,
) -> Result<R>
where
R: for<'de> Deserialize<'de>,
{
self.delete(path, Some(body)).await?.to_json().await
}
}
25 changes: 21 additions & 4 deletions cargo-shuttle/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ pub enum Command {
/// Deploy a Shuttle service
Deploy(DeployArgs),
/// Manage deployments of a Shuttle service
#[command(subcommand)]
#[command(subcommand, visible_alias = "depl")]
Deployment(DeploymentCommand),
/// View the status of a Shuttle service
Status,
Expand All @@ -115,17 +115,18 @@ pub enum Command {
/// View logs of a Shuttle service
Logs(LogsArgs),
/// Manage projects on Shuttle
#[command(subcommand)]
#[command(subcommand, visible_alias = "proj")]
Project(ProjectCommand),
/// Manage resources
#[command(subcommand)]
#[command(subcommand, visible_alias = "res")]
Resource(ResourceCommand),
/// BETA: Manage SSL certificates for custom domains
#[command(subcommand)]
#[command(subcommand, visible_alias = "cert", hide = true)]
Certificate(CertificateCommand),
/// Remove cargo build artifacts in the Shuttle environment
Clean,
/// BETA: Show info about your Shuttle account
#[command(visible_alias = "acc", hide = true)]
Account,
/// Login to the Shuttle platform
Login(LoginArgs),
Expand Down Expand Up @@ -162,6 +163,7 @@ pub struct TableArgs {
#[derive(Parser)]
pub enum DeploymentCommand {
/// List the deployments for a service
#[command(visible_alias = "ls")]
List {
#[arg(long, default_value = "1")]
/// Which page to display
Expand All @@ -180,12 +182,14 @@ pub enum DeploymentCommand {
id: Option<String>,
},
/// BETA: Stop running deployment(s)
#[command(hide = true)]
Stop,
}

#[derive(Parser)]
pub enum ResourceCommand {
/// List the resources for a project
#[command(visible_alias = "ls")]
List {
#[command(flatten)]
table: TableArgs,
Expand All @@ -195,6 +199,7 @@ pub enum ResourceCommand {
show_secrets: bool,
},
/// Delete a resource
#[command(visible_alias = "rm")]
Delete {
/// Type of the resource to delete.
/// Use the string in the 'Type' column as displayed in the `resource list` command.
Expand All @@ -213,10 +218,19 @@ pub enum CertificateCommand {
domain: String,
},
/// List the certificates for a project
#[command(visible_alias = "ls")]
List {
#[command(flatten)]
table: TableArgs,
},
/// Delete an SSL certificate
#[command(visible_alias = "rm")]
Delete {
/// Domain name
domain: String,
#[command(flatten)]
confirmation: ConfirmationArgs,
},
}

#[derive(Parser)]
Expand All @@ -235,6 +249,7 @@ pub enum ProjectCommand {
/// Destroy and create an environment for this project on Shuttle
Restart(ProjectStartArgs),
/// List all projects you have access to
#[command(visible_alias = "ls")]
List {
// deprecated args, kept around to not break
#[arg(long, hide = true)]
Expand All @@ -246,6 +261,7 @@ pub enum ProjectCommand {
table: TableArgs,
},
/// Delete a project and all linked data
#[command(visible_alias = "rm")]
Delete(ConfirmationArgs),
}

Expand Down Expand Up @@ -277,6 +293,7 @@ pub struct LogoutArgs {
#[arg(long)]
pub reset_api_key: bool,
}

#[derive(Parser, Default)]
pub struct DeployArgs {
/// BETA: Deploy this Docker image instead of building one
Expand Down
2 changes: 1 addition & 1 deletion cargo-shuttle/src/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ fn copy_dirs(src: &Path, dest: &Path, git_policy: GitDir) -> Result<()> {
);
} else {
// Copy this file.
fs::copy(&entry.path(), &entry_dest)?;
fs::copy(entry.path(), &entry_dest)?;
}
} else if entry_type.is_symlink() {
println!("Warning: symlink '{entry_name}' is ignored");
Expand Down
58 changes: 54 additions & 4 deletions cargo-shuttle/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,11 @@ impl Shuttle {
},
Command::Certificate(cmd) => match cmd {
CertificateCommand::Add { domain } => self.add_certificate(domain).await,
CertificateCommand::List { .. } => todo!(),
CertificateCommand::List { table } => self.list_certificates(table).await,
CertificateCommand::Delete {
domain,
confirmation: ConfirmationArgs { yes },
} => self.delete_certificate(domain, yes).await,
},
Command::Project(cmd) => match cmd {
ProjectCommand::Start(ProjectStartArgs { idle_minutes }) => {
Expand Down Expand Up @@ -1305,13 +1309,59 @@ impl Shuttle {
Ok(CommandOutcome::Ok)
}

async fn list_certificates(&self, _table_args: TableArgs) -> Result<CommandOutcome> {
let client = self.client.as_ref().unwrap();
let certs = client
.list_certificates_beta(self.ctx.project_name())
.await?;

// TODO: make table
println!("{:?}", certs);

Ok(CommandOutcome::Ok)
}
async fn add_certificate(&self, domain: String) -> Result<CommandOutcome> {
let client = self.client.as_ref().unwrap();
client
let cert = client
.add_certificate_beta(self.ctx.project_name(), domain.clone())
.await?;

println!("Added certificate for {domain}");
// TODO: Make nicer
println!("{:?}", cert);

Ok(CommandOutcome::Ok)
}
async fn delete_certificate(&self, domain: String, no_confirm: bool) -> Result<CommandOutcome> {
let client = self.client.as_ref().unwrap();

if !no_confirm {
println!(
"{}",
formatdoc!(
"
WARNING:
Delete the certificate for {}?",
domain
)
.bold()
.red()
);
if !Confirm::with_theme(&ColorfulTheme::default())
.with_prompt("Are you sure?")
.default(false)
.interact()
.unwrap()
{
return Ok(CommandOutcome::Ok);
}
}

client
.delete_certificate_beta(self.ctx.project_name(), domain.clone())
.await?;

println!("Deleted certificate for {domain}");

Ok(CommandOutcome::Ok)
}
Expand Down Expand Up @@ -1702,7 +1752,7 @@ impl Shuttle {

fn find_available_port(run_args: &mut RunArgs, services_len: usize) {
let default_port = run_args.port;
'outer: for port in (run_args.port..=std::u16::MAX).step_by(services_len.max(10)) {
'outer: for port in (run_args.port..=u16::MAX).step_by(services_len.max(10)) {
for inner_port in port..(port + services_len as u16) {
if !portpicker::is_free_tcp(inner_port) {
continue 'outer;
Expand All @@ -1727,7 +1777,7 @@ impl Shuttle {
}
fn find_available_port_beta(run_args: &mut RunArgs) {
let original_port = run_args.port;
for port in (run_args.port..=std::u16::MAX).step_by(10) {
for port in (run_args.port..=u16::MAX).step_by(10) {
if !portpicker::is_free_tcp(port) {
continue;
}
Expand Down
Loading