Skip to content

Commit

Permalink
feat(cargo-shuttle): check project name available
Browse files Browse the repository at this point in the history
  • Loading branch information
jonaro00 committed Sep 27, 2023
1 parent e78f6c7 commit c019e7c
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 10 deletions.
2 changes: 1 addition & 1 deletion cargo-shuttle/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ pub struct InitArgs {
#[arg(default_value = ".", value_parser = OsStringValueParser::new().try_map(parse_init_path))]
pub path: PathBuf,

/// Whether to create the environment for this project on Shuttle
/// Whether to start the container for this project on Shuttle, and claim the project name
#[arg(long)]
pub create_env: bool,
#[command(flatten)]
Expand Down
56 changes: 49 additions & 7 deletions cargo-shuttle/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,12 +232,39 @@ impl Shuttle {
It will be hosted at ${{project_name}}.shuttleapp.rs, so choose something unique!
"
);
// TODO: Check whether the project name is still available
project_args.name = Some(
Input::with_theme(&theme)
loop {
// not using validate_with due to being blocking
let p: ProjectName = Input::with_theme(&theme)
.with_prompt("Project name")
.interact()?,
);
.interact()?;
enum NameCheck {
Available,
Taken,
Failed,
}
// TODO replace this with self.client after PR 1275
match reqwest::get(format!("{}/project/name/{}", self.ctx.api_url(), p))
.await
.map(|r| match r.status() {
reqwest::StatusCode::NOT_FOUND => NameCheck::Available,
reqwest::StatusCode::OK => NameCheck::Taken,
_ => NameCheck::Failed,
})
.unwrap_or(NameCheck::Failed)
{
NameCheck::Available => {
project_args.name = Some(p);
break;
}
NameCheck::Taken => {
println!("Project name already taken: '{}'", p.to_string());
}
NameCheck::Failed => {
println!("Failed to check if project name is available.");
break;
}
}
}
println!();
}

Expand Down Expand Up @@ -327,9 +354,18 @@ impl Shuttle {
true
} else {
let should_create = Confirm::with_theme(&theme)
.with_prompt("Do you want to create the project environment on Shuttle?")
.with_prompt(format!(
"Do you want to start the project container on Shuttle and claim the project name '{}'?",
project_args
.name
.as_ref()
.expect("to have a project name provided")),
)
.default(true)
.interact()?;
if !should_create {
println!("Note: The project name will not be claimed until you start the project with `cargo shuttle project start`.")
}
println!();
should_create
};
Expand Down Expand Up @@ -1452,7 +1488,13 @@ impl Shuttle {
"getting project status failed repeteadly",
)
})?;
println!("{project}");
println!(
"{project} (idle minutes: {})",
project
.idle_minutes
.map(|i| i.to_string())
.unwrap_or("<unknown>".to_owned())
);
}

Ok(())
Expand Down
2 changes: 1 addition & 1 deletion common/src/models/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ impl From<ErrorKind> for ApiError {
ErrorKind::UserAlreadyExists => (StatusCode::BAD_REQUEST, "user already exists"),
ErrorKind::ProjectNotFound => (
StatusCode::NOT_FOUND,
"project not found. Run `cargo shuttle project start` to create a new project.",
"project not found. Make sure you are the owner of this project name. Run `cargo shuttle project start` to create a new project.",
),
ErrorKind::ProjectNotReady => (
StatusCode::SERVICE_UNAVAILABLE,
Expand Down
32 changes: 31 additions & 1 deletion gateway/src/api/latest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use std::time::Duration;
use axum::body::Body;
use axum::extract::{Extension, Path, Query, State};
use axum::handler::Handler;
use axum::http::Request;
use axum::http::{Request, StatusCode};
use axum::middleware::from_extractor;
use axum::response::Response;
use axum::routing::{any, get, post};
Expand Down Expand Up @@ -126,6 +126,35 @@ async fn get_project(
Ok(AxumJson(response))
}

#[instrument(skip(service))]
#[utoipa::path(
get,
path = "/projects/name/{project_name}",
responses(
(status = 200, description = "Project name is taken."),
(status = 404, description = "Project name is free."),
(status = 500, description = "Server internal error.")
),
params(
("project_name" = String, Path, description = "The project name to check."),
)
)]
async fn check_project_name(
State(RouterState { service, .. }): State<RouterState>,
Path(project_name): Path<ProjectName>,
) -> StatusCode {
match service.project_name_exists(&project_name).await {
Ok(_) => StatusCode::OK,
Err(e) => {
if e.kind == ErrorKind::ProjectNotFound {
StatusCode::NOT_FOUND
} else {
StatusCode::INTERNAL_SERVER_ERROR
}
}
}
}

#[utoipa::path(
get,
path = "/projects",
Expand Down Expand Up @@ -861,6 +890,7 @@ impl ApiBuilder {
.delete(destroy_project.layer(ScopedLayer::new(vec![Scope::ProjectCreate])))
.post(create_project.layer(ScopedLayer::new(vec![Scope::ProjectCreate]))),
)
.route("/projects/name/:project_name", get(check_project_name))
.route("/projects/:project_name/*any", any(route_project))
.route("/stats/load", post(post_load).delete(delete_load))
.nest("/admin", admin_routes);
Expand Down
10 changes: 10 additions & 0 deletions gateway/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,16 @@ impl GatewayService {
.ok_or_else(|| Error::from_kind(ErrorKind::ProjectNotFound))
}

pub async fn project_name_exists(&self, project_name: &ProjectName) -> Result<(), Error> {
let a = query("SELECT project_name FROM projects WHERE project_name=?1")
.bind(project_name)
.fetch_optional(&self.db)
.await?;
println!("{}", a.is_some());
a.map(|_| ())
.ok_or_else(|| Error::from_kind(ErrorKind::ProjectNotFound))
}

pub async fn iter_user_projects_detailed(
&self,
account_name: &AccountName,
Expand Down

0 comments on commit c019e7c

Please sign in to comment.