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

refactor: switch away from cargo package #507

Merged
12 changes: 12 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions cargo-shuttle/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ dirs = "4.0.0"
flate2 = "1.0.25"
futures = "0.3.25"
headers = "0.3.8"
ignore = "0.4.18"
indoc = "1.0.7"
log = "0.4.17"
openssl = { version = '0.10', optional = true }
Expand Down
3 changes: 0 additions & 3 deletions cargo-shuttle/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,6 @@ pub struct AuthArgs {

#[derive(Parser)]
pub struct DeployArgs {
/// allow dirty working directories to be packaged
#[clap(long)]
pub allow_dirty: bool,
/// allows pre-deploy tests to be skipped
#[clap(long)]
pub no_test: bool,
Expand Down
224 changes: 121 additions & 103 deletions cargo-shuttle/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,25 @@ use std::io::Write;
use std::io::{self, stdout};
use std::net::{Ipv4Addr, SocketAddr};
use std::path::{Path, PathBuf};
use std::rc::Rc;

use anyhow::{anyhow, Context, Result};
pub use args::{Args, Command, DeployArgs, InitArgs, ProjectArgs, RunArgs};
use args::{AuthArgs, LoginArgs};
use cargo::core::resolver::CliFeatures;
use cargo::core::Workspace;
use cargo::ops::{PackageOpts, Packages};
use cargo_metadata::Message;
use clap::CommandFactory;
use clap_complete::{generate, Shell};
use config::RequestContext;
use crossterm::style::Stylize;
use factory::LocalFactory;
use flate2::read::GzDecoder;
use flate2::write::GzEncoder;
use flate2::Compression;
use futures::StreamExt;
use ignore::overrides::OverrideBuilder;
use ignore::WalkBuilder;
use shuttle_common::models::secret;
use shuttle_service::loader::{build_crate, Loader};
use shuttle_service::Logger;
use tar::{Archive, Builder};
use tar::Builder;
use tokio::sync::mpsc;
use tracing::trace;
use uuid::Uuid;
Expand Down Expand Up @@ -345,11 +342,7 @@ impl Shuttle {
}

async fn deploy(&self, args: DeployArgs, client: &Client) -> Result<CommandOutcome> {
let package_file = self
.run_cargo_package(args.allow_dirty)
.context("failed to package cargo project")?;

let data = self.package_secret(package_file)?;
let data = self.make_archive()?;

let deployment = client
.deploy(data, self.ctx.project_name(), args.no_test)
Expand Down Expand Up @@ -430,67 +423,53 @@ impl Shuttle {
Ok(())
}

// Packages the cargo project and returns a File to that file
fn run_cargo_package(&self, allow_dirty: bool) -> Result<File> {
let config = cargo::util::config::Config::default()?;
fn make_archive(&self) -> Result<Vec<u8>> {
let encoder = GzEncoder::new(Vec::new(), Compression::fast());
chesedo marked this conversation as resolved.
Show resolved Hide resolved
let mut tar = Builder::new(encoder);

let working_directory = self.ctx.working_directory();
let path = working_directory.join("Cargo.toml");

let ws = Workspace::new(&path, &config)?;
let opts = PackageOpts {
config: &config,
list: false,
check_metadata: true,
allow_dirty,
keep_going: false,
verify: false,
jobs: None,
to_package: Packages::Default,
targets: vec![],
cli_features: CliFeatures {
features: Rc::new(Default::default()),
all_features: false,
uses_default_features: true,
},
};

let locks = cargo::ops::package(&ws, &opts)?.expect("unwrap ok here");
let owned = locks.get(0).unwrap().file().try_clone()?;
Ok(owned)
}

fn package_secret(&self, file: File) -> Result<Vec<u8>> {
let tar_read = GzDecoder::new(file);
let mut archive_read = Archive::new(tar_read);
let tar_write = GzEncoder::new(Vec::new(), Compression::best());
let mut archive_write = Builder::new(tar_write);

for entry in archive_read.entries()? {
let entry = entry?;
let path = entry.path()?;
let file_name = path.components().nth(1).unwrap();

if file_name.as_os_str() == "Secrets.toml" {
println!(
"{}: you may want to fix this",
"Secrets.toml might be tracked by your version control".yellow()
);
let base_directory = working_directory
.parent()
.context("get parent directory of crate")?;

// Make sure the target folder is excluded at all times
let overrides = OverrideBuilder::new(working_directory)
.add("!target/")
.context("add `!target/` override")?
.build()
.context("build an override")?;

for dir_entry in WalkBuilder::new(working_directory)
.hidden(false)
.overrides(overrides)
.build()
{
let dir_entry = dir_entry.context("get directory entry")?;

// It's not possible to add a directory to an archive
if dir_entry.file_type().context("get file type")?.is_dir() {
continue;
}

archive_write.append(&entry.header().clone(), entry)?;
let path = dir_entry
.path()
.strip_prefix(base_directory)
.context("strip the base of the archive entry")?;

tar.append_path_with_name(dir_entry.path(), path)
.context("archive entry")?;
}

// Make sure to add any `Secrets.toml` files
let secrets_path = self.ctx.working_directory().join("Secrets.toml");
if secrets_path.exists() {
archive_write
.append_path_with_name(secrets_path, Path::new("shuttle").join("Secrets.toml"))?;
tar.append_path_with_name(secrets_path, Path::new("shuttle").join("Secrets.toml"))?;
}

let encoder = archive_write.into_inner()?;
let data = encoder.finish()?;
let encoder = tar.into_inner().context("get encoder from tar archive")?;
let bytes = encoder.finish().context("finish up encoder")?;

Ok(data)
Ok(bytes)
}
}

Expand All @@ -502,20 +481,49 @@ pub enum CommandOutcome {
#[cfg(test)]
mod tests {
use flate2::read::GzDecoder;
use shuttle_common::project::ProjectName;
use tar::Archive;
use tempfile::TempDir;

use crate::args::ProjectArgs;
use crate::Shuttle;
use std::fs::{canonicalize, File};
use std::io::Write;
use std::fs::{self, canonicalize};
use std::path::PathBuf;
use std::str::FromStr;

fn path_from_workspace_root(path: &str) -> PathBuf {
PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap())
.join("..")
.join(path)
}

fn get_archive_entries(mut project_args: ProjectArgs) -> Vec<String> {
let mut shuttle = Shuttle::new().unwrap();
shuttle.load_project(&mut project_args).unwrap();

let archive = shuttle.make_archive().unwrap();

// Make sure the Secrets.toml file is not initially present
let tar = GzDecoder::new(&archive[..]);
let mut archive = Archive::new(tar);

archive
.entries()
.unwrap()
.map(|entry| {
entry
.unwrap()
.path()
.unwrap()
.components()
.skip(1)
.collect::<PathBuf>()
.display()
.to_string()
})
.collect()
}

#[test]
fn find_root_directory_returns_proper_directory() {
let working_directory = path_from_workspace_root("examples/axum/hello-world/src");
Expand Down Expand Up @@ -545,62 +553,72 @@ mod tests {
}

#[test]
fn secrets_file_is_archived() {
fn make_archive_include_secrets() {
let working_directory =
canonicalize(path_from_workspace_root("examples/rocket/secrets")).unwrap();

let mut secrets_file = File::create(working_directory.join("Secrets.toml")).unwrap();
secrets_file
.write_all(b"MY_API_KEY = 'the contents of my API key'")
.unwrap();
fs::write(
working_directory.join("Secrets.toml"),
"MY_API_KEY = 'the contents of my API key'",
)
.unwrap();

let mut project_args = ProjectArgs {
let project_args = ProjectArgs {
working_directory,
name: None,
};

let mut shuttle = Shuttle::new().unwrap();
shuttle.load_project(&mut project_args).unwrap();
let entries = get_archive_entries(project_args);

let file = shuttle.run_cargo_package(true).unwrap();
assert_eq!(
chesedo marked this conversation as resolved.
Show resolved Hide resolved
entries,
vec![
"src/lib.rs",
"README.md",
"Cargo.toml",
"Shuttle.toml",
"Secrets.toml.example",
".gitignore",
"Secrets.toml",
]
);
}

// Make sure the Secrets.toml file is not initially present
let tar = GzDecoder::new(file);
let mut archive = Archive::new(tar);
#[test]
fn make_archive_respect_ignore() {
let tmp_dir = TempDir::new().unwrap();
let working_directory = tmp_dir.path();

for entry in archive.entries().unwrap() {
let entry = entry.unwrap();
let path = entry.path().unwrap();
let name = path.components().nth(1).unwrap().as_os_str();
fs::write(working_directory.join(".env"), "API_KEY = 'blabla'").unwrap();
fs::write(working_directory.join(".ignore"), ".env").unwrap();
fs::write(working_directory.join("Cargo.toml"), "[package]").unwrap();

assert!(
name != "Secrets.toml",
"no Secrets.toml file should be in the initial archive: {:?}",
path
);
}
let project_args = ProjectArgs {
working_directory: working_directory.to_path_buf(),
name: Some(ProjectName::from_str("secret").unwrap()),
};

let file = shuttle.run_cargo_package(true).unwrap();
let new_file = shuttle.package_secret(file).unwrap();
let mut found_secrets_file = false;
let entries = get_archive_entries(project_args);

// This time the Secrets.toml file should be present
let tar = flate2::bufread::GzDecoder::new(&new_file[..]);
let mut archive = Archive::new(tar);
assert_eq!(entries, vec!["Cargo.toml", ".ignore"]);
}

for entry in archive.entries().unwrap() {
let entry = entry.unwrap();
let path = entry.path().unwrap();
let name = path.components().nth(1).unwrap().as_os_str();
#[test]
fn make_archive_ignore_target_folder() {
let tmp_dir = TempDir::new().unwrap();
let working_directory = tmp_dir.path();

if name == "Secrets.toml" {
found_secrets_file = true;
}
}
fs::create_dir_all(working_directory.join("target")).unwrap();
fs::write(working_directory.join("target").join("binary"), "12345").unwrap();
fs::write(working_directory.join("Cargo.toml"), "[package]").unwrap();

assert!(
found_secrets_file,
"Secrets.toml was not added to the archive"
);
let project_args = ProjectArgs {
working_directory: working_directory.to_path_buf(),
name: Some(ProjectName::from_str("exclude_target").unwrap()),
};

let entries = get_archive_entries(project_args);

assert_eq!(entries, vec!["Cargo.toml"]);
}
}