Skip to content

Commit

Permalink
Merge branch 'shuttle-hq:main' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
sourabpramanik authored Apr 10, 2024
2 parents 264a2f1 + 1922b0e commit eb7a98d
Show file tree
Hide file tree
Showing 21 changed files with 673 additions and 261 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,5 @@ yarn.lock
*.wasm
*.sqlite*
.envrc

/private
4 changes: 3 additions & 1 deletion Cargo.lock

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

2 changes: 2 additions & 0 deletions admin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ publish = false

[dependencies]
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"] }
Expand Down
6 changes: 6 additions & 0 deletions admin/src/args.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::{fs, io, path::PathBuf};

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

#[derive(Parser, Debug)]
pub struct Args {
Expand All @@ -27,6 +28,11 @@ pub enum Command {
/// Manage project names
ProjectNames,

ChangeProjectOwner {
project_name: String,
new_user_id: UserId,
},

/// Viewing and managing stats
#[command(subcommand)]
Stats(StatsCommand),
Expand Down
29 changes: 22 additions & 7 deletions admin/src/client.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use anyhow::{Context, Result};
use anyhow::{bail, Context, Result};
use bytes::Bytes;
use serde::{de::DeserializeOwned, Serialize};
use shuttle_common::models::{admin::ProjectResponse, stats, ToJson};
use tracing::trace;
Expand Down Expand Up @@ -73,6 +74,15 @@ impl Client {
self.get("/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?;

Ok(())
}

pub async fn get_load(&self) -> Result<stats::LoadResponse> {
self.get("/admin/stats/load").await
}
Expand Down Expand Up @@ -130,15 +140,20 @@ impl Client {
.context("failed to extract json body from delete response")
}

async fn get<R: DeserializeOwned>(&self, path: &str) -> Result<R> {
reqwest::Client::new()
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("failed to make get request")?
.to_json()
.await
.context("failed to post text body from response")
.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")
}
}
4 changes: 4 additions & 0 deletions admin/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
use std::{fs, path::PathBuf};

pub fn get_api_key() -> String {
if let Ok(s) = std::env::var("SHUTTLE_API_KEY") {
return s;
}

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");

Expand Down
159 changes: 48 additions & 111 deletions admin/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@ use shuttle_admin::{
client::Client,
config::get_api_key,
};
use std::{
collections::{hash_map::RandomState, HashMap},
fmt::Write,
};
use shuttle_backends::project_name::ProjectName;
use tracing::trace;

#[tokio::main]
Expand All @@ -21,131 +18,63 @@ async fn main() {
let api_key = get_api_key();
let client = Client::new(args.api_url.clone(), api_key);

let res = match args.command {
Command::Revive => client.revive().await.expect("revive to succeed"),
Command::Destroy => client.destroy().await.expect("destroy to succeed"),
match args.command {
Command::Revive => {
let s = client.revive().await.expect("revive to succeed");
println!("{s}");
}
Command::Destroy => {
let s = client.destroy().await.expect("destroy to succeed");
println!("{s}");
}
Command::Acme(AcmeCommand::CreateAccount { email, acme_server }) => {
let account = client
.acme_account_create(&email, acme_server)
.await
.expect("to create ACME account");

let mut res = String::new();
writeln!(res, "Details of ACME account are as follow. Keep this safe as it will be needed to create certificates in the future").unwrap();
writeln!(res, "{}", serde_json::to_string_pretty(&account).unwrap()).unwrap();

res
println!("Details of ACME account are as follow. Keep this safe as it will be needed to create certificates in the future");
println!("{}", serde_json::to_string_pretty(&account).unwrap());
}
Command::Acme(AcmeCommand::Request {
fqdn,
project,
credentials,
}) => client
.acme_request_certificate(&fqdn, &project, &credentials)
.await
.expect("to get a certificate challenge response"),
}) => {
let s = client
.acme_request_certificate(&fqdn, &project, &credentials)
.await
.expect("to get a certificate challenge response");
println!("{s}");
}
Command::Acme(AcmeCommand::RenewCustomDomain {
fqdn,
project,
credentials,
}) => client
.acme_renew_custom_domain_certificate(&fqdn, &project, &credentials)
.await
.expect("to get a certificate challenge response"),
Command::Acme(AcmeCommand::RenewGateway { credentials }) => client
.acme_renew_gateway_certificate(&credentials)
.await
.expect("to get a certificate challenge response"),
}) => {
let s = client
.acme_renew_custom_domain_certificate(&fqdn, &project, &credentials)
.await
.expect("to get a certificate challenge response");
println!("{s}");
}
Command::Acme(AcmeCommand::RenewGateway { credentials }) => {
let s = client
.acme_renew_gateway_certificate(&credentials)
.await
.expect("to get a certificate challenge response");
println!("{s}");
}
Command::ProjectNames => {
let projects = client
.get_projects()
.await
.expect("to get list of projects");

let projects: HashMap<String, String, RandomState> = HashMap::from_iter(
projects
.into_iter()
.map(|project| (project.project_name, project.account_name)),
);

let mut res = String::new();

for (project_name, account_name) in &projects {
let mut issues = Vec::new();
let cleaned_name = project_name.to_lowercase();

// Were there any uppercase characters
if &cleaned_name != project_name {
// Since there were uppercase characters, will the new name clash with any existing projects
if let Some(other_account) = projects.get(&cleaned_name) {
if other_account == account_name {
issues.push(
"changing to lower case will clash with same owner".to_string(),
);
} else {
issues.push(format!(
"changing to lower case will clash with another owner: {other_account}"
));
}
}
}

let cleaned_underscore = cleaned_name.replace('_', "-");
// Were there any underscore cleanups
if cleaned_underscore != cleaned_name {
// Since there were underscore cleanups, will the new name clash with any existing projects
if let Some(other_account) = projects.get(&cleaned_underscore) {
if other_account == account_name {
issues
.push("cleaning underscore will clash with same owner".to_string());
} else {
issues.push(format!(
"cleaning underscore will clash with another owner: {other_account}"
));
}
}
}

let cleaned_separator_name = cleaned_underscore.trim_matches('-');
// Were there any dash cleanups
if cleaned_separator_name != cleaned_underscore {
// Since there were dash cleanups, will the new name clash with any existing projects
if let Some(other_account) = projects.get(cleaned_separator_name) {
if other_account == account_name {
issues.push("cleaning dashes will clash with same owner".to_string());
} else {
issues.push(format!(
"cleaning dashes will clash with another owner: {other_account}"
));
}
}
}

// Are reserved words used
match cleaned_separator_name {
"shuttleapp" | "shuttle" => issues.push("is a reserved name".to_string()),
_ => {}
}

// Is it longer than 63 chars
if cleaned_separator_name.len() > 63 {
issues.push("final name is too long".to_string());
}

// Only report of problem projects
if !issues.is_empty() {
writeln!(res, "{project_name}")
.expect("to write name of project name having issues");

for issue in issues {
writeln!(res, "\t- {issue}").expect("to write issue with project name");
}

writeln!(res).expect("to write a new line");
for p in projects {
if !ProjectName::is_valid(&p.project_name) {
println!("{}", p.project_name);
}
}

res
}
Command::Stats(StatsCommand::Load { clear }) => {
let resp = if clear {
Expand All @@ -156,16 +85,24 @@ async fn main() {

let has_capacity = if resp.has_capacity { "a" } else { "no" };

format!(
println!(
"Currently {} builds are running and there is {} capacity for new builds",
resp.builds_count, has_capacity
)
}
Command::IdleCch => {
client.idle_cch().await.expect("cch projects to be idled");
"Idled CCH projects".to_string()
println!("Idled CCH projects")
}
Command::ChangeProjectOwner {
project_name,
new_user_id,
} => {
client
.change_project_owner(&project_name, &new_user_id)
.await
.unwrap();
println!("Changed project owner: {project_name} -> {new_user_id}")
}
};

println!("{res}");
}
3 changes: 2 additions & 1 deletion backends/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@ opentelemetry-appender-tracing = { workspace = true }
opentelemetry-http = { workspace = true }
opentelemetry-otlp = { workspace = true }
pin-project = { workspace = true }
permit-client-rs = { git = "https://github.com/shuttle-hq/permit-client-rs", rev = "27c7759" }
permit-client-rs = { git = "https://github.com/shuttle-hq/permit-client-rs", rev = "19085ba" }
permit-pdp-client-rs = { git = "https://github.com/shuttle-hq/permit-pdp-client-rs", rev = "37c7296" }
portpicker = { workspace = true, optional = true }
reqwest = { workspace = true, features = ["json"] }
# keep locked to not accidentally invalidate someone's project name
# higher versions have a lot more false positives
rustrict = { version = "=0.7.12" }
serde = { workspace = true, features = ["derive", "std"] }
serde_json = { workspace = true }
Expand Down
Loading

0 comments on commit eb7a98d

Please sign in to comment.