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: find (soon to be) invalid project names #479

Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 4 additions & 0 deletions admin/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@ pub enum Command {
/// Try to revive projects in the crashed state
Revive,

/// Manage custom domains
#[command(subcommand)]
Acme(AcmeCommand),

/// Manage project names
ProjectNames,
}

#[derive(Subcommand, Debug)]
Expand Down
16 changes: 16 additions & 0 deletions admin/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ impl Client {
self.post(&path, Some(credentials)).await
}

pub async fn list_invalid_project_names(&self) -> Result<Vec<(String, Vec<String>)>> {
self.get("/admin/invalid-names").await
}

async fn post<T: Serialize, R: DeserializeOwned>(
&self,
path: &str,
Expand All @@ -59,4 +63,16 @@ impl Client {
.await
.context("failed to extract json body from post response")
}

async fn get<R: DeserializeOwned>(&self, path: &str) -> Result<R> {
reqwest::Client::new()
.get(format!("{}{}", self.api_url, path))
.bearer_auth(&self.api_key)
.send()
.await
.context("failed to make post request")?
.to_json()
.await
.context("failed to post text body from response")
}
}
20 changes: 20 additions & 0 deletions admin/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,26 @@ async fn main() {
.await
.expect("to get a certificate challenge response")
}
Command::ProjectNames => {
let projects = client
.list_invalid_project_names()
.await
.expect("get invalid project names");

let mut res = String::new();

for (project, issues) in projects {
writeln!(res, "{project}").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");
}

res
}
};

println!("{res}");
Expand Down
83 changes: 83 additions & 0 deletions gateway/src/api/latest.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;

Expand Down Expand Up @@ -220,6 +221,87 @@ async fn request_acme_certificate(
Ok("Certificate created".to_string())
}

async fn invalid_project_names(
chesedo marked this conversation as resolved.
Show resolved Hide resolved
_: Admin,
Extension(service): Extension<Arc<GatewayService>>,
) -> Result<AxumJson<Vec<(String, Vec<String>)>>, Error> {
let projects: HashMap<String, String, std::collections::hash_map::RandomState> =
HashMap::from_iter(
service
.iter_projects_with_user()
.await?
.map(|(project_name, account_name)| (project_name.0, account_name.0)),
);
let mut result = Vec::with_capacity(projects.len());

for (project_name, account_name) in &projects {
let mut output = 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 {
output.push("changing to lower case will clash with same owner".to_string());
} else {
output.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 {
output.push("cleaning underscore will clash with same owner".to_string());
} else {
output.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 {
output.push("cleaning dashes will clash with same owner".to_string());
} else {
output.push(format!(
"cleaning dashes will clash with another owner: {other_account}"
));
}
}
}

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

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

// Only report of problem projects
if !output.is_empty() {
result.push((project_name.to_string(), output));
}
}

Ok(AxumJson(result))
}

pub fn make_api(
service: Arc<GatewayService>,
acme_client: AcmeClient,
Expand All @@ -241,6 +323,7 @@ pub fn make_api(
.route("/admin/revive", post(revive_projects))
.route("/admin/acme/:email", post(create_acme_account))
.route("/admin/acme/request/:project_name/:fqdn", post(request_acme_certificate))
.route("/admin/invalid-names", get(invalid_project_names))
.layer(Extension(service))
.layer(Extension(acme_client))
.layer(Extension(sender))
Expand Down
24 changes: 24 additions & 0 deletions gateway/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,22 @@ impl GatewayService {
Ok(custom_domain)
}

pub async fn iter_projects_with_user(
&self,
) -> Result<impl Iterator<Item = (ProjectName, AccountName)>, Error> {
let iter = query("SELECT project_name, account_name FROM projects")
.fetch_all(&self.db)
.await?
.into_iter()
.map(|row| {
(
row.try_get("project_name").unwrap(),
row.try_get("account_name").unwrap(),
)
});
Ok(iter)
}

pub fn context(&self) -> GatewayContext {
self.provider.context()
}
Expand Down Expand Up @@ -613,6 +629,14 @@ pub mod tests {
assert!(creating_same_project_name(&project, &matrix));

assert_eq!(svc.find_project(&matrix).await.unwrap(), project);
assert_eq!(
svc.iter_projects_with_user()
.await
.unwrap()
.next()
.expect("to get one project with its user"),
(matrix.clone(), neo.clone())
);

let mut work = svc
.new_task()
Expand Down