diff --git a/api-client/src/lib.rs b/api-client/src/lib.rs index 5fde1096d..c0c6cd2e0 100644 --- a/api-client/src/lib.rs +++ b/api-client/src/lib.rs @@ -14,7 +14,7 @@ use shuttle_common::log::{LogsRange, LogsResponseBeta}; use shuttle_common::models::deployment::{ DeploymentListResponseBeta, DeploymentRequest, DeploymentRequestBeta, UploadArchiveResponseBeta, }; -use shuttle_common::models::project::ProjectListResponseBeta; +use shuttle_common::models::project::{ProjectCreateRequestBeta, ProjectListResponseBeta}; use shuttle_common::models::{deployment, project, service, team, user}; use shuttle_common::resource::{ ProvisionResourceRequestBeta, ResourceListResponseBeta, ResourceResponseBeta, @@ -265,8 +265,13 @@ impl ShuttleApiClient { .context("failed to make create project request") } pub async fn create_project_beta(&self, name: &str) -> Result { - self.post_json(format!("/projects/{name}"), None::<()>) - .await + self.post_json( + "/projects", + Some(ProjectCreateRequestBeta { + name: name.to_string(), + }), + ) + .await } pub async fn clean_project(&self, project: &str) -> Result { diff --git a/cargo-shuttle/src/args.rs b/cargo-shuttle/src/args.rs index 8b542a1c8..08c886b0b 100644 --- a/cargo-shuttle/src/args.rs +++ b/cargo-shuttle/src/args.rs @@ -30,16 +30,16 @@ pub struct ShuttleArgs { pub project_args: ProjectArgs, /// Run this command against the API at the supplied URL /// (allows targeting a custom deployed instance for this command only, mainly for development) - #[arg(long, env = "SHUTTLE_API")] + #[arg(global = true, long, env = "SHUTTLE_API")] pub api_url: Option, /// Disable network requests that are not strictly necessary. Limits some features. - #[arg(long, env = "SHUTTLE_OFFLINE")] + #[arg(global = true, long, env = "SHUTTLE_OFFLINE")] pub offline: bool, /// Turn on tracing output for Shuttle libraries. (WARNING: can print sensitive data) - #[arg(long, env = "SHUTTLE_DEBUG")] + #[arg(global = true, long, env = "SHUTTLE_DEBUG")] pub debug: bool, /// Target Shuttle's development environment - #[arg(long, env = "SHUTTLE_BETA", hide = true)] + #[arg(global = true, long, env = "SHUTTLE_BETA", hide = true)] pub beta: bool, #[command(subcommand)] @@ -52,9 +52,10 @@ pub struct ProjectArgs { /// Specify the working directory #[arg(global = true, long, visible_alias = "wd", default_value = ".", value_parser = OsStringValueParser::new().try_map(parse_path))] pub working_directory: PathBuf, - /// Specify the name of the project (overrides crate name) - #[arg(global = true, long)] - pub name: Option, + /// Specify the name or id of the project (overrides crate name) + #[arg(global = true, long = "name", visible_alias = "id")] + // in alpha mode, this is always a name + pub name_or_id: Option, } impl ProjectArgs { @@ -269,6 +270,8 @@ pub enum ProjectCommand { /// Delete a project and all linked data #[command(visible_alias = "rm")] Delete(ConfirmationArgs), + /// Link this workspace to a Shuttle project + Link, } #[derive(Parser, Debug)] @@ -597,7 +600,7 @@ mod tests { fn workspace_path() { let project_args = ProjectArgs { working_directory: path_from_workspace_root("examples/axum/hello-world/src"), - name: None, + name_or_id: None, }; assert_eq!( @@ -610,7 +613,7 @@ mod tests { fn project_name() { let project_args = ProjectArgs { working_directory: path_from_workspace_root("examples/axum/hello-world/src"), - name: None, + name_or_id: None, }; assert_eq!( @@ -625,7 +628,7 @@ mod tests { working_directory: path_from_workspace_root( "examples/rocket/workspace/hello-world/src", ), - name: None, + name_or_id: None, }; assert_eq!( diff --git a/cargo-shuttle/src/config.rs b/cargo-shuttle/src/config.rs index 7e14cd54f..12304d704 100644 --- a/cargo-shuttle/src/config.rs +++ b/cargo-shuttle/src/config.rs @@ -9,6 +9,7 @@ use shuttle_common::{constants::API_URL_DEFAULT, ApiKey}; use tracing::trace; use crate::args::ProjectArgs; +use crate::init::create_or_update_ignore_file; /// Helper trait for dispatching fs ops for different config files pub trait ConfigManager: Sized { @@ -144,11 +145,22 @@ impl GlobalConfig { } } -/// Project-local config for things like customizing project name +/// Shuttle.toml schema (User-facing project-local config) #[derive(Deserialize, Serialize, Default)] pub struct ProjectConfig { + // unused on new platform pub name: Option, pub assets: Option>, + // unused in cargo-shuttle, used in new platform builder. + // is used here to validate the type if used. + pub build_assets: Option>, +} + +/// .shuttle/config.toml schema (internal project-local config) +#[derive(Deserialize, Serialize, Default)] +pub struct InternalProjectConfig { + // should be in internal local config + pub id: Option, } /// A handler for configuration files. The type parameter `M` is the [`ConfigManager`] which handles @@ -233,6 +245,7 @@ where pub struct RequestContext { global: Config, project: Option>, + project_internal: Option>, api_url: Option, } @@ -249,10 +262,82 @@ impl RequestContext { Ok(Self { global, project: None, + project_internal: None, api_url: None, }) } + pub fn load_local_internal(&mut self, project_args: &ProjectArgs) -> Result<()> { + let workspace_path = project_args + .workspace_path() + .unwrap_or(project_args.working_directory.clone()); + + trace!( + "looking for .shuttle/config.toml in {}", + workspace_path.display() + ); + let local_manager = + LocalConfigManager::new(workspace_path, ".shuttle/config.toml".to_string()); + let mut project_internal = Config::new(local_manager); + if !project_internal.exists() { + trace!("no local .shuttle/config.toml found"); + project_internal.replace(InternalProjectConfig::default()); + } else { + trace!("found a local .shuttle/config.toml"); + project_internal.open()?; + } + + let config = project_internal.as_mut().unwrap(); + + // Project id is preferred in this order: + // 1. Name given on command line + // 2. Name from .shuttle/config.toml file + match (&project_args.name_or_id, &config.id) { + // Command-line name parameter trumps everything + (Some(id_from_args), _) => { + trace!("using command-line project id"); + config.id = Some(id_from_args.clone()); + } + // If key exists in config then keep it as it is + (None, Some(_)) => { + trace!("using .shuttle/config.toml project id"); + } + (None, None) => { + trace!("no project id in args or config found"); + } + }; + + self.project_internal = Some(project_internal); + + Ok(()) + } + + pub fn set_project_id(&mut self, id: String) { + *self.project_internal.as_mut().unwrap().as_mut().unwrap() = + InternalProjectConfig { id: Some(id) }; + } + + pub fn save_local_internal(&mut self) -> Result<()> { + // if self.project_internal.is_some() { + self.project_internal.as_ref().unwrap().save()?; + // } + + // write updated gitignore file to root of workspace + // TODO: assumes git is used + create_or_update_ignore_file( + &self + .project + .as_ref() + .unwrap() + .manager + .working_directory + .join(".gitignore"), + ) + .context("Failed to create .gitignore file")?; + + Ok(()) + } + /// Load the project configuration at the given `working_directory` /// /// Ensures that if `--name` is not specified on the command-line, and either the project @@ -277,7 +362,6 @@ impl RequestContext { trace!("looking for Shuttle.toml in {}", workspace_path.display()); let local_manager = LocalConfigManager::new(workspace_path, "Shuttle.toml".to_string()); let mut project = Config::new(local_manager); - if !project.exists() { trace!("no local Shuttle.toml found"); project.replace(ProjectConfig::default()); @@ -292,8 +376,8 @@ impl RequestContext { // 1. Name given on command line // 2. Name from Shuttle.toml file // 3. Name from Cargo.toml package if it's a crate - // 3. Name from the workspace directory if it's a workspace - match (&project_args.name, &config.name) { + // 4. Name from the workspace directory if it's a workspace + match (&project_args.name_or_id, &config.name) { // Command-line name parameter trumps everything (Some(name_from_args), _) => { trace!("using command-line project name"); @@ -309,6 +393,7 @@ impl RequestContext { config.name = Some(project_args.project_name()?); } }; + Ok(project) } @@ -373,6 +458,7 @@ impl RequestContext { self.global.as_mut().unwrap().clear_api_key(); self.global.save() } + /// Get the current project name. /// /// # Panics @@ -400,6 +486,33 @@ impl RequestContext { .assets .as_ref() } + + /// Check if the current project id has been loaded. + pub fn project_id_found(&self) -> bool { + self.project_internal + .as_ref() + .unwrap() + .as_ref() + .unwrap() + .id + .is_some() + } + + /// Get the current project id. + /// + /// # Panics + /// Panics if the internal project configuration has not been loaded. + pub fn project_id(&self) -> &str { + self.project_internal + .as_ref() + .unwrap() + .as_ref() + .unwrap() + .id + .as_ref() + .unwrap() + .as_str() + } } #[cfg(test)] @@ -424,7 +537,7 @@ mod tests { fn get_local_config_finds_name_in_shuttle_toml() { let project_args = ProjectArgs { working_directory: path_from_workspace_root("examples/axum/hello-world/"), - name: None, + name_or_id: None, }; let local_config = RequestContext::get_local_config(&project_args).unwrap(); @@ -436,7 +549,7 @@ mod tests { fn get_local_config_finds_name_from_workspace_dir() { let project_args = ProjectArgs { working_directory: path_from_workspace_root("examples/rocket/workspace/hello-world/"), - name: None, + name_or_id: None, }; let local_config = RequestContext::get_local_config(&project_args).unwrap(); @@ -448,7 +561,7 @@ mod tests { fn setting_name_overrides_name_in_config() { let project_args = ProjectArgs { working_directory: path_from_workspace_root("examples/axum/hello-world/"), - name: Some("my-fancy-project-name".to_owned()), + name_or_id: Some("my-fancy-project-name".to_owned()), }; let local_config = RequestContext::get_local_config(&project_args).unwrap(); diff --git a/cargo-shuttle/src/init.rs b/cargo-shuttle/src/init.rs index c4b9b32d2..c9f3175d2 100644 --- a/cargo-shuttle/src/init.rs +++ b/cargo-shuttle/src/init.rs @@ -51,7 +51,7 @@ pub fn generate_project( // if the crate name was not updated, set it in Shuttle.toml instead edit_shuttle_toml(&path, (!crate_name_set).then_some(name)) .context("Failed to edit Shuttle.toml")?; - create_ignore_file(&path, if no_git { ".ignore" } else { ".gitignore" }) + create_or_update_ignore_file(&path.join(if no_git { ".ignore" } else { ".gitignore" })) .context("Failed to create .gitignore file")?; copy_dirs(&path, &dest, GitDir::Ignore) @@ -273,18 +273,17 @@ fn edit_shuttle_toml(path: &Path, set_name: Option<&str>) -> Result<()> { Ok(()) } -/// Adds any missing recommended ignore rules to .gitignore or .ignore depending on if git is used. -fn create_ignore_file(path: &Path, name: &str) -> Result<()> { - let path = path.join(name); - let mut contents = std::fs::read_to_string(&path).unwrap_or_default(); +/// Adds any missing recommended ignore rules to an ignore file (usually .gitignore) +pub fn create_or_update_ignore_file(path: &Path) -> Result<()> { + let mut contents = std::fs::read_to_string(path).unwrap_or_default(); - for rule in ["/target", ".shuttle-storage", "Secrets*.toml"] { + for rule in ["/target", ".shuttle*", "Secrets*.toml"] { if !contents.lines().any(|l| l == rule) { writeln!(&mut contents, "{rule}")?; } } - std::fs::write(&path, contents)?; + std::fs::write(path, contents)?; Ok(()) } diff --git a/cargo-shuttle/src/lib.rs b/cargo-shuttle/src/lib.rs index a70fbd885..e1afe7558 100644 --- a/cargo-shuttle/src/lib.rs +++ b/cargo-shuttle/src/lib.rs @@ -177,7 +177,9 @@ impl Shuttle { } } else if matches!( args.cmd, - Command::Deployment(DeploymentCommand::Stop) | Command::Account + Command::Deployment(DeploymentCommand::Stop) + | Command::Account + | Command::Project(ProjectCommand::Link) ) { bail!("This command is not supported on the legacy platform. Set --beta or SHUTTLE_BETA=true."); } @@ -191,30 +193,6 @@ impl Shuttle { } self.ctx.set_api_url(args.api_url); - // All commands that need to know which project is being handled - if matches!( - args.cmd, - Command::Deploy(..) - | Command::Deployment(..) - | Command::Resource(..) - | Command::Certificate(..) - | Command::Project( - // ProjectCommand::List does not need to know which project we are in - ProjectCommand::Start { .. } - | ProjectCommand::Stop { .. } - | ProjectCommand::Restart { .. } - | ProjectCommand::Status { .. } - | ProjectCommand::Delete { .. } - ) - | Command::Stop - | Command::Clean - | Command::Status - | Command::Logs { .. } - | Command::Run(..) - ) { - self.load_project(&args.project_args)?; - } - // All commands that call the API if matches!( args.cmd, @@ -249,6 +227,35 @@ impl Shuttle { } } + // All commands that need to know which project is being handled + if matches!( + args.cmd, + Command::Deploy(..) + | Command::Deployment(..) + | Command::Resource(..) + | Command::Certificate(..) + | Command::Project( + // ProjectCommand::List does not need to know which project we are in + ProjectCommand::Start { .. } + | ProjectCommand::Stop { .. } + | ProjectCommand::Restart { .. } + | ProjectCommand::Status { .. } + | ProjectCommand::Delete { .. } + | ProjectCommand::Link + ) + | Command::Stop + | Command::Clean + | Command::Status + | Command::Logs { .. } + | Command::Run(..) + ) { + self.load_project( + &args.project_args, + matches!(args.cmd, Command::Project(ProjectCommand::Link)), + ) + .await?; + } + let res = match args.cmd { Command::Init(init_args) => { self.init( @@ -319,7 +326,7 @@ impl Shuttle { Command::Project(cmd) => match cmd { ProjectCommand::Start(ProjectStartArgs { idle_minutes }) => { if self.beta { - self.project_start_beta().await + self.project_create_beta().await } else { self.project_start(idle_minutes).await } @@ -337,6 +344,7 @@ impl Shuttle { ProjectCommand::List { table, .. } => self.projects_list(table).await, ProjectCommand::Stop => self.project_stop().await, ProjectCommand::Delete(ConfirmationArgs { yes }) => self.project_delete(yes).await, + ProjectCommand::Link => Ok(()), // logic is done in `load_local` }, Command::Upgrade { preview } => update_cargo_shuttle(preview).await, }; @@ -409,7 +417,7 @@ impl Shuttle { let git_template = args.git_template()?; let no_git = args.no_git; - let needs_name = project_args.name.is_none(); + let needs_name = project_args.name_or_id.is_none(); let needs_template = git_template.is_none(); let needs_path = !provided_path_to_init; let needs_login = self.ctx.api_key().is_err() && args.login_args.api_key.is_none(); @@ -439,7 +447,7 @@ impl Shuttle { let mut prev_name: Option = None; loop { // prompt if interactive - let name: String = if let Some(name) = project_args.name.clone() { + let name: String = if let Some(name) = project_args.name_or_id.clone() { name } else { // not using `validate_with` due to being blocking. @@ -450,7 +458,7 @@ impl Shuttle { let force_name = args.force_name || (needs_name && prev_name.as_ref().is_some_and(|prev| prev == &name)); if force_name { - project_args.name = Some(name); + project_args.name_or_id = Some(name); break; } // validate and take action based on result @@ -477,9 +485,12 @@ impl Shuttle { // 3. Confirm the project directory let path = if needs_path { - let path = args - .path - .join(project_args.name.as_ref().expect("name should be set")); + let path = args.path.join( + project_args + .name_or_id + .as_ref() + .expect("name should be set"), + ); loop { println!("Where should we create this project?"); @@ -645,7 +656,7 @@ impl Shuttle { crate::init::generate_project( path.clone(), project_args - .name + .name_or_id .as_ref() .expect("to have a project name provided"), &template, @@ -665,7 +676,7 @@ impl Shuttle { .with_prompt(format!( r#"Claim the project name "{}" by starting a project container on Shuttle?"#, project_args - .name + .name_or_id .as_ref() .expect("to have a project name provided") )) @@ -686,7 +697,7 @@ impl Shuttle { // so `load_project` is ran with the correct project path project_args.working_directory.clone_from(&path); - self.load_project(&project_args)?; + self.load_project(&project_args, false).await?; self.project_start(DEFAULT_IDLE_MINUTES).await?; } @@ -730,7 +741,7 @@ impl Shuttle { Ok(true) => { // inner value is inverted on beta if self.beta { - project_args.name = Some(name); + project_args.name_or_id = Some(name); return true; } @@ -741,7 +752,7 @@ impl Shuttle { } // not possible on beta Ok(false) => { - project_args.name = Some(name); + project_args.name_or_id = Some(name); true } @@ -757,7 +768,7 @@ impl Shuttle { } // Else, the API error was about something else. // Ignore and keep going to not prevent the flow of the init command. - project_args.name = Some(name); + project_args.name_or_id = Some(name); println!( "{}", "Failed to check if project name is available.".yellow() @@ -768,10 +779,88 @@ impl Shuttle { } } - pub fn load_project(&mut self, project_args: &ProjectArgs) -> Result<()> { - trace!("loading project arguments: {project_args:?}"); + pub async fn load_project(&mut self, project_args: &ProjectArgs, link_cmd: bool) -> Result<()> { + trace!("project arguments: {project_args:?}"); - self.ctx.load_local(project_args) + self.ctx.load_local(project_args)?; + if self.beta { + // load project id from file if exists + self.ctx.load_local_internal(project_args)?; + // translate project name to project id if a name was given + if let Some(name) = project_args.name_or_id.as_ref() { + if !name.starts_with("proj_") { + trace!("mapping project name to project id"); + let proj = self + .client + .as_ref() + .unwrap() + .get_projects_list_beta() + .await? + .projects + .into_iter() + .find(|p| p.name == *name); + if let Some(proj) = proj { + trace!("found project by name"); + self.ctx.set_project_id(proj.id); + } + } + // if called from Link command, command-line override is saved to file + if link_cmd { + eprintln!("Linking to project {}", self.ctx.project_id()); + self.ctx.save_local_internal()?; + return Ok(()); + } + } + // if project id is still not known or an explicit linking is wanted, start the linking prompt + if !self.ctx.project_id_found() || link_cmd { + self.project_link(None).await?; + } + } + + Ok(()) + } + + async fn project_link(&mut self, id_or_name: Option) -> Result<()> { + let client = self.client.as_ref().unwrap(); + let projs = client.get_projects_list_beta().await?.projects; + + let theme = ColorfulTheme::default(); + + let proj = if let Some(id_or_name) = id_or_name { + projs + .into_iter() + .find(|p| p.id == id_or_name || p.name == id_or_name) + .ok_or(anyhow!("Did not find project '{id_or_name}'."))? + } else { + eprintln!("Which project do you want to link this directory to?"); + + let mut items = projs.iter().map(|p| p.name.clone()).collect::>(); + items.extend_from_slice(&["[CREATE NEW]".to_string()]); + let index = Select::with_theme(&theme) + .items(&items) + .default(0) + .interact()?; + + // if last item selected (Create new) + if index == projs.len() { + let name: String = Input::with_theme(&theme) + .with_prompt("Project name") + .interact()?; + + let project = client.create_project_beta(&name).await?; + eprintln!("Created project '{}' with id {}", project.name, project.id); + + project + } else { + projs[index].clone() + } + }; + + eprintln!("Linking to project '{}' with id {}", proj.name, proj.id); + self.ctx.set_project_id(proj.id); + self.ctx.save_local_internal()?; + + Ok(()) } async fn account(&self) -> Result<()> { @@ -852,11 +941,11 @@ impl Shuttle { async fn stop_beta(&self) -> Result<()> { let client = self.client.as_ref().unwrap(); - let p = self.ctx.project_name(); - let res = client.stop_service_beta(p).await?; + let pid = self.ctx.project_id(); + let res = client.stop_service_beta(pid).await?; println!("{res}"); wait_with_spinner(2000, |_, pb| async move { - let deployment = client.get_current_deployment_beta(p).await?; + let deployment = client.get_current_deployment_beta(pid).await?; let get_cleanup = |d: Option| { move || { @@ -996,16 +1085,13 @@ impl Shuttle { } // TODO: implement logs range let client = self.client.as_ref().unwrap(); - let proj_name = self.ctx.project_name(); + let pid = self.ctx.project_id(); let logs = if args.all_deployments { - client.get_project_logs_beta(proj_name).await?.logs + client.get_project_logs_beta(pid).await?.logs } else { let id = if args.latest { // Find latest deployment (not always an active one) - let deployments = client - .get_deployments_beta(proj_name, 1, 1) - .await? - .deployments; + let deployments = client.get_deployments_beta(pid, 1, 1).await?.deployments; let Some(most_recent) = deployments.first() else { println!("No deployments found"); return Ok(()); @@ -1015,14 +1101,14 @@ impl Shuttle { } else if let Some(id) = args.id { id } else { - let Some(current) = client.get_current_deployment_beta(proj_name).await? else { + let Some(current) = client.get_current_deployment_beta(pid).await? else { println!("No deployments found"); return Ok(()); }; eprintln!("Getting logs from: {}", current.id); current.id }; - client.get_deployment_logs_beta(proj_name, &id).await?.logs + client.get_deployment_logs_beta(pid, &id).await?.logs }; for log in logs { if args.raw { @@ -1139,7 +1225,7 @@ impl Shuttle { let deployments_len = if self.beta { let mut deployments = client - .get_deployments_beta(proj_name, page as i32, limit as i32) + .get_deployments_beta(self.ctx.project_id(), page as i32, limit as i32) .await? .deployments; let page_hint = if deployments.len() == limit as usize { @@ -1191,16 +1277,11 @@ impl Shuttle { let client = self.client.as_ref().unwrap(); if self.beta { + let pid = self.ctx.project_id(); let deployment = match deployment_id { - Some(id) => { - client - .get_deployment_beta(self.ctx.project_name(), &id) - .await - } + Some(id) => client.get_deployment_beta(pid, &id).await, None => { - let d = client - .get_current_deployment_beta(self.ctx.project_name()) - .await?; + let d = client.get_current_deployment_beta(pid).await?; let Some(d) = d else { println!("No deployment found"); return Ok(()); @@ -1247,16 +1328,10 @@ impl Shuttle { async fn resources_list_beta(&self, table_args: TableArgs, show_secrets: bool) -> Result<()> { let client = self.client.as_ref().unwrap(); - let resources = client - .get_service_resources_beta(self.ctx.project_name()) - .await? - .resources; - let table = get_resource_tables_beta( - resources.as_slice(), - self.ctx.project_name(), - table_args.raw, - show_secrets, - ); + let pid = self.ctx.project_id(); + let resources = client.get_service_resources_beta(pid).await?.resources; + let table = + get_resource_tables_beta(resources.as_slice(), pid, table_args.raw, show_secrets); println!("{table}"); @@ -1295,7 +1370,7 @@ impl Shuttle { if self.beta { let msg = client - .delete_service_resource_beta(self.ctx.project_name(), resource_type) + .delete_service_resource_beta(self.ctx.project_id(), resource_type) .await?; println!("{msg}"); } else { @@ -1321,7 +1396,7 @@ impl Shuttle { async fn list_certificates(&self, table_args: TableArgs) -> Result<()> { let client = self.client.as_ref().unwrap(); let certs = client - .list_certificates_beta(self.ctx.project_name()) + .list_certificates_beta(self.ctx.project_id()) .await? .certificates; @@ -1333,7 +1408,7 @@ impl Shuttle { async fn add_certificate(&self, domain: String) -> Result<()> { let client = self.client.as_ref().unwrap(); let cert = client - .add_certificate_beta(self.ctx.project_name(), domain.clone()) + .add_certificate_beta(self.ctx.project_id(), domain.clone()) .await?; println!("Added certificate for {}", cert.subject); @@ -1366,7 +1441,7 @@ impl Shuttle { } let msg = client - .delete_certificate_beta(self.ctx.project_name(), domain.clone()) + .delete_certificate_beta(self.ctx.project_id(), domain.clone()) .await?; println!("{msg}"); @@ -2262,7 +2337,7 @@ impl Shuttle { let deployment = client .deploy_beta( - project_name, + self.ctx.project_id(), DeploymentRequestBeta::Image(deployment_req_image_beta), ) .await?; @@ -2376,8 +2451,10 @@ impl Shuttle { if self.beta { // TODO: upload secrets separately + let pid = self.ctx.project_id(); + eprintln!("Uploading code..."); - let arch = client.upload_archive_beta(project_name, archive).await?; + let arch = client.upload_archive_beta(pid, archive).await?; deployment_req_buildarch_beta.archive_version_id = arch.archive_version_id; deployment_req_buildarch_beta.build_meta = Some(BuildMetaBeta { git_commit_id: deployment_req.git_commit_id, @@ -2389,7 +2466,7 @@ impl Shuttle { eprintln!("Creating deployment..."); let deployment = client .deploy_beta( - project_name, + pid, DeploymentRequestBeta::BuildArchive(deployment_req_buildarch_beta), ) .await?; @@ -2401,7 +2478,7 @@ impl Shuttle { let id = &deployment.id; wait_with_spinner(2000, |_, pb| async move { - let deployment = client.get_deployment_beta(project_name, id).await?; + let deployment = client.get_deployment_beta(pid, id).await?; let state = deployment.state.clone(); pb.set_message(deployment.to_string_summary_colored()); @@ -2417,11 +2494,7 @@ impl Shuttle { | DeploymentStateBeta::Stopping | DeploymentStateBeta::Unknown => Ok(Some(cleanup)), DeploymentStateBeta::Failed => { - for log in client - .get_deployment_logs_beta(project_name, id) - .await? - .logs - { + for log in client.get_deployment_logs_beta(pid, id).await?.logs { if args.raw { println!("{}", log.line); } else { @@ -2439,16 +2512,12 @@ impl Shuttle { deployment_req.data = archive; let deployment = client - .deploy(self.ctx.project_name(), deployment_req) + .deploy(project_name, deployment_req) .await .map_err(suggestions::deploy::deploy_request_failure)?; let mut stream = client - .get_logs_ws( - self.ctx.project_name(), - &deployment.id.to_string(), - LogsRange::All, - ) + .get_logs_ws(project_name, &deployment.id.to_string(), LogsRange::All) .await .map_err(|err| { suggestions::deploy::deployment_setup_failure( @@ -2568,11 +2637,7 @@ impl Shuttle { // the terminal isn't completely spammed sleep(Duration::from_millis(100)).await; stream = client - .get_logs_ws( - self.ctx.project_name(), - &deployment.id.to_string(), - LogsRange::All, - ) + .get_logs_ws(project_name, &deployment.id.to_string(), LogsRange::All) .await .map_err(|err| { suggestions::deploy::deployment_setup_failure( @@ -2589,7 +2654,7 @@ impl Shuttle { sleep(Duration::from_millis(500)).await; let deployment = client - .get_deployment_details(self.ctx.project_name(), &deployment.id) + .get_deployment_details(project_name, &deployment.id) .await .map_err(|err| { suggestions::deploy::deployment_setup_failure( @@ -2635,11 +2700,9 @@ impl Shuttle { bail!(""); } - let service = client.get_service(self.ctx.project_name()).await?; - let resources = client - .get_service_resources(self.ctx.project_name()) - .await?; - let resources = get_resource_tables(&resources, self.ctx.project_name(), false, false); + let service = client.get_service(project_name).await?; + let resources = client.get_service_resources(project_name).await?; + let resources = get_resource_tables(&resources, project_name, false, false); println!("{resources}{service}"); @@ -2700,7 +2763,7 @@ impl Shuttle { Ok(()) } - async fn project_start_beta(&self) -> Result<()> { + async fn project_create_beta(&self) -> Result<()> { let client = self.client.as_ref().unwrap(); let name = self.ctx.project_name(); let project = client.create_project_beta(name).await?; @@ -2813,7 +2876,7 @@ impl Shuttle { } async fn project_status_beta(&self) -> Result<()> { let client = self.client.as_ref().unwrap(); - let project = client.get_project_beta(self.ctx.project_name()).await?; + let project = client.get_project_beta(self.ctx.project_id()).await?; print!("{}", project.to_string_colored()); Ok(()) @@ -2911,7 +2974,7 @@ impl Shuttle { } if self.beta { - client.delete_project_beta(self.ctx.project_name()).await? + client.delete_project_beta(self.ctx.project_id()).await? } else { client .delete_project(self.ctx.project_name()) @@ -3295,13 +3358,13 @@ mod tests { dunce::canonicalize(path).unwrap() } - fn get_archive_entries( + async fn get_archive_entries( project_args: ProjectArgs, deploy_args: DeployArgs, zip: bool, ) -> Vec { let mut shuttle = Shuttle::new(crate::Binary::CargoShuttle).unwrap(); - shuttle.load_project(&project_args).unwrap(); + shuttle.load_project(&project_args, false).await.unwrap(); let archive = shuttle .make_archive(deploy_args.secret_args.secrets, zip) @@ -3334,8 +3397,8 @@ mod tests { } } - #[test] - fn make_archive_respect_rules() { + #[tokio::test] + async fn make_archive_respect_rules() { let working_directory = canonicalize(path_from_workspace_root( "cargo-shuttle/tests/resources/archiving", )) @@ -3353,9 +3416,10 @@ mod tests { let project_args = ProjectArgs { working_directory: working_directory.clone(), - name: Some("archiving-test".to_owned()), + name_or_id: Some("archiving-test".to_owned()), }; - let mut entries = get_archive_entries(project_args.clone(), Default::default(), false); + let mut entries = + get_archive_entries(project_args.clone(), Default::default(), false).await; entries.sort(); let expected = vec![ @@ -3378,7 +3442,7 @@ mod tests { assert_eq!(entries, expected); // check that zip behaves the same way - let mut entries = get_archive_entries(project_args.clone(), Default::default(), true); + let mut entries = get_archive_entries(project_args.clone(), Default::default(), true).await; entries.sort(); assert_eq!(entries, expected); @@ -3392,7 +3456,8 @@ mod tests { ..Default::default() }, false, - ); + ) + .await; entries.sort(); assert_eq!( @@ -3417,15 +3482,15 @@ mod tests { ); } - #[test] - fn finds_workspace_root() { + #[tokio::test] + async fn finds_workspace_root() { let project_args = ProjectArgs { working_directory: path_from_workspace_root("examples/axum/hello-world/src"), - name: None, + name_or_id: None, }; let mut shuttle = Shuttle::new(crate::Binary::CargoShuttle).unwrap(); - shuttle.load_project(&project_args).unwrap(); + shuttle.load_project(&project_args, false).await.unwrap(); assert_eq!( project_args.working_directory, diff --git a/cargo-shuttle/tests/integration/main.rs b/cargo-shuttle/tests/integration/main.rs index 3fa1d63aa..7deb67f62 100644 --- a/cargo-shuttle/tests/integration/main.rs +++ b/cargo-shuttle/tests/integration/main.rs @@ -15,7 +15,7 @@ async fn cargo_shuttle_command(cmd: Command, working_directory: &str) -> anyhow: api_url: Some("http://shuttle.invalid:80".to_string()), project_args: ProjectArgs { working_directory, - name: None, + name_or_id: None, }, offline: false, debug: false, diff --git a/common-tests/src/cargo_shuttle.rs b/common-tests/src/cargo_shuttle.rs index c496531b9..9cb37d14e 100644 --- a/common-tests/src/cargo_shuttle.rs +++ b/common-tests/src/cargo_shuttle.rs @@ -41,7 +41,7 @@ pub async fn cargo_shuttle_run(working_directory: &str, external: bool) -> Strin api_url: Some("http://shuttle.invalid:80".to_string()), project_args: ProjectArgs { working_directory: working_directory.clone(), - name: None, + name_or_id: None, }, offline: false, debug: false, diff --git a/common/src/models/project.rs b/common/src/models/project.rs index c1f628b9f..5e1bd9a29 100644 --- a/common/src/models/project.rs +++ b/common/src/models/project.rs @@ -27,6 +27,12 @@ pub struct Response { pub is_admin: bool, } +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +#[typeshare::typeshare] +pub struct ProjectCreateRequestBeta { + pub name: String, +} + #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] #[typeshare::typeshare] pub struct ProjectResponseBeta { diff --git a/common/types.ts b/common/types.ts index 7a2eb0141..cf62d59ce 100644 --- a/common/types.ts +++ b/common/types.ts @@ -130,6 +130,10 @@ export interface LogsResponseBeta { logs: LogItemBeta[]; } +export interface ProjectCreateRequestBeta { + name: string; +} + export interface ProjectResponseBeta { id: string; /** Project owner */