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 project linking #1885

Merged
merged 11 commits into from
Sep 30, 2024
11 changes: 8 additions & 3 deletions api-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -265,8 +265,13 @@ impl ShuttleApiClient {
.context("failed to make create project request")
}
pub async fn create_project_beta(&self, name: &str) -> Result<project::ProjectResponseBeta> {
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<String> {
Expand Down
23 changes: 13 additions & 10 deletions cargo-shuttle/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
/// 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)]
Expand All @@ -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<String>,
/// 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<String>,
}

impl ProjectArgs {
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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!(
Expand All @@ -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!(
Expand All @@ -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!(
Expand Down
127 changes: 120 additions & 7 deletions cargo-shuttle/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<String>,
pub assets: Option<Vec<String>>,
// unused in cargo-shuttle, used in new platform builder.
// is used here to validate the type if used.
pub build_assets: Option<Vec<String>>,
}

/// .shuttle/config.toml schema (internal project-local config)
#[derive(Deserialize, Serialize, Default)]
pub struct InternalProjectConfig {
// should be in internal local config
pub id: Option<String>,
}

/// A handler for configuration files. The type parameter `M` is the [`ConfigManager`] which handles
Expand Down Expand Up @@ -233,6 +245,7 @@ where
pub struct RequestContext {
global: Config<GlobalConfigManager, GlobalConfig>,
project: Option<Config<LocalConfigManager, ProjectConfig>>,
project_internal: Option<Config<LocalConfigManager, InternalProjectConfig>>,
api_url: Option<String>,
}

Expand All @@ -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
Expand All @@ -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());
Expand All @@ -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");
Expand All @@ -309,6 +393,7 @@ impl RequestContext {
config.name = Some(project_args.project_name()?);
}
};

Ok(project)
}

Expand Down Expand Up @@ -373,6 +458,7 @@ impl RequestContext {
self.global.as_mut().unwrap().clear_api_key();
self.global.save()
}

/// Get the current project name.
///
/// # Panics
Expand Down Expand Up @@ -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)]
Expand All @@ -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();
Expand All @@ -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();
Expand All @@ -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();
Expand Down
13 changes: 6 additions & 7 deletions cargo-shuttle/src/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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(())
}
Expand Down
Loading