Skip to content

Commit

Permalink
Merge pull request #3 from ereslibre/ls-rm
Browse files Browse the repository at this point in the history
Implement list, pull force and rm
  • Loading branch information
flavio committed Apr 14, 2022
2 parents afb5ee4 + 9178212 commit 8d1b9f0
Show file tree
Hide file tree
Showing 8 changed files with 240 additions and 47 deletions.
19 changes: 19 additions & 0 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions Cargo.toml
Expand Up @@ -11,8 +11,10 @@ clap = { version = "3.1", features = [ "derive", "env" ]
directories = "4.0"
kube-conf = "0.2.0"
lazy_static = "1.4.0"
pathdiff = "0.2"
policy-fetcher = { git = "https://github.com/kubewarden/policy-fetcher", tag = "v0.6.1" }
regex = "1"
term-table = "1.3"
thiserror = "1.0.30"
tokio = "1.17"
tracing = "0.1"
Expand Down
24 changes: 17 additions & 7 deletions src/cli.rs
Expand Up @@ -22,6 +22,23 @@ pub(crate) struct Native {

#[derive(Debug, Subcommand)]
pub(crate) enum NativeCommands {
/// List
List,
/// Pull
#[clap(arg_required_else_help = true)]
Pull {
/// URI for the WebAssembly module to pull
uri: String,
/// Remove an existing module with the same name, if any
#[clap(short, long)]
force: bool,
},
/// Rm
#[clap(arg_required_else_help = true)]
Rm {
/// Name of the WebAssembly module to remove
module: String,
},
/// Run
#[clap(arg_required_else_help = true)]
Run {
Expand All @@ -31,11 +48,4 @@ pub(crate) enum NativeCommands {
#[clap(last = true)]
wasm_args: Vec<String>,
},
/// Pull
#[clap(arg_required_else_help = true)]
Pull {
/// URI for the WebAssembly module to pull
uri: String,
},
// TODO: add a rm command
}
53 changes: 53 additions & 0 deletions src/ls.rs
@@ -0,0 +1,53 @@
use anyhow::Result;
use pathdiff::diff_paths;
use std::{fs, path::Path};
use term_table::{row::Row, Table, TableStyle};

use crate::store::{ALL_MODULES_STORE_ROOT, STORE_ROOT};

pub(crate) fn ls() {
let mut table = Table::new();
table.style = TableStyle::simple();
table.add_row(Row::new(vec!["Name", "Location"]));
for module in fs::read_dir(ALL_MODULES_STORE_ROOT.as_path())
.expect("could not read store root")
.flatten()
{
if let Some(module_name) = module.file_name().to_str() {
table.add_row(Row::new(vec![
module_name,
&module_store_location(&module.path()).expect("invalid filename"),
]));
}
}
println!("{}", table.render());
}

// Given a module location in the directory where symlinks to all
// modules are located, give back the URI resembling where this module
// was pulled from, or the path to the local filesystem where this
// module is located if it wasn't pulled from a remote location
fn module_store_location(module_path: &Path) -> Result<String> {
let module_path = std::fs::read_link(module_path)?;
// If this module was added from somehwere in the filesystem
// (outside of the store), just return it as it is
if !module_path.starts_with(STORE_ROOT.as_path()) {
return Ok(format!(
"{} (not in the store)",
module_path.to_str().expect("invalid path")
));
}
let path = diff_paths(module_path, STORE_ROOT.as_path()).expect("failed to diff paths");
let mut component_iterator = path.components();
let scheme = component_iterator.next().expect("invalid path");
Ok(component_iterator.fold(
format!("{}:/", scheme.as_os_str().to_str().expect("invalid path")),
|acc, element| {
format!(
"{}/{}",
acc,
element.as_os_str().to_str().expect("invalid path")
)
},
))
}
21 changes: 18 additions & 3 deletions src/main.rs
Expand Up @@ -8,16 +8,20 @@ use tracing_subscriber::{fmt, EnvFilter};

mod cli;
mod errors;
mod store;
mod wasm_host;

mod ls;
mod pull;
mod rm;
mod run;
mod wasm_host;

use clap::Parser;
use cli::{NativeCommands, BINARY_NAME, KREW_WASM_VERBOSE_ENV};

use errors::KrewWapcError;

use pull::ALL_MODULES_STORE_ROOT;
use store::ALL_MODULES_STORE_ROOT;

lazy_static! {
// Useful when developing the project: `cargo run` leads to a
Expand Down Expand Up @@ -50,6 +54,8 @@ fn setup_logging(verbose: bool) {
async fn main() {
// setup logging

store::ensure();

let args: Vec<String> = env::args().collect();
if BINARY_NAMES.contains(&args[0]) {
// native mode
Expand Down Expand Up @@ -99,7 +105,16 @@ async fn main() {

async fn run_native(cli: cli::Native) {
match cli.command {
NativeCommands::List => ls::ls(),
NativeCommands::Pull { uri, force } => {
let force_pull = if force {
pull::ForcePull::ForcePull
} else {
pull::ForcePull::DoNotForcePull
};
pull::pull(&uri, force_pull).await
}
NativeCommands::Rm { module } => rm::rm(&module),
NativeCommands::Run { module, wasm_args } => run::run(module, wasm_args),
NativeCommands::Pull { uri } => pull::pull(uri).await,
}
}
64 changes: 27 additions & 37 deletions src/pull.rs
@@ -1,50 +1,26 @@
use directories::{ProjectDirs, UserDirs};
use lazy_static::lazy_static;
use policy_fetcher::{fetch_policy, PullDestination};
use regex::Regex;
use std::{
path::{Path, PathBuf},
process,
};
use std::{path::Path, process};

use crate::cli::BINARY_NAME;
use crate::store::{ALL_MODULES_STORE_ROOT, BIN_ROOT, STORE_ROOT};

lazy_static! {
static ref BIN_ROOT: PathBuf = UserDirs::new()
.expect("cannot find home directory for user")
.home_dir()
.join(".krew-wasm")
.join("bin");
static ref TAG_REMOVER: Regex = Regex::new(r":[^:]+$").unwrap();
static ref STORE_ROOT: PathBuf = ProjectDirs::from("io.krew-wasm", "", "krew-wasm")
.expect("cannot find project dirs")
.cache_dir()
.join("krew-wasm-store");
pub static ref ALL_MODULES_STORE_ROOT: PathBuf = STORE_ROOT.join("all");
}

pub(crate) async fn pull(uri: String) {
// Try to create the kubectl plugin bin path.
std::fs::create_dir_all(BIN_ROOT.as_path()).unwrap_or_else(|err| {
panic!(
"could not create alias binary root at {}: {}",
BIN_ROOT.display(),
err
)
});

// Try to create the "all modules" root on the store. Used
// to look for modules given a name.
let all_modules_store_root = &ALL_MODULES_STORE_ROOT;
std::fs::create_dir_all(all_modules_store_root.as_path())
.expect("could not create top level store path for all modules");
#[derive(PartialEq)]
pub(crate) enum ForcePull {
ForcePull,
DoNotForcePull,
}

pub(crate) async fn pull(uri: &str, force_pull: ForcePull) {
// Fetch the wasm module
let module = fetch_policy(&uri, PullDestination::Store(STORE_ROOT.clone()), None, None)
let module = fetch_policy(uri, PullDestination::Store(STORE_ROOT.clone()), None, None)
.await
.expect("failed pulling module");

// Create the webassembly module symlink in the "all modules" root
let module_store_path = module.local_path;
let module_name = TAG_REMOVER
.replace(
Expand All @@ -58,15 +34,29 @@ pub(crate) async fn pull(uri: String) {
.to_string();
let module_name = module_name.strip_suffix(".wasm").unwrap_or(&module_name);

if Path::exists(&all_modules_store_root.join(&module_name)) {
eprintln!("there is already a module with this name. Run `{} rm {}` to remove it, and run this command again", BINARY_NAME, module_name);
process::exit(1);
if Path::exists(&ALL_MODULES_STORE_ROOT.join(&module_name)) {
if force_pull == ForcePull::DoNotForcePull {
eprintln!("there is already a module with this name ({}). You can pull with the `-f` flag to overwrite the existing module", module_name);
process::exit(1);
}
// When forcing the pull, rm the module name, so all the
// cleaning logic of the store is triggered. Then, fetch the
// module again. This is not neat, and the policy fetcher
// could be improved to provide the path where the module
// would have been placed to know before pulling if something
// existed on the path already. Given force pulling does not
// happen so often, just pull the policy again.
crate::rm::rm(module_name);
fetch_policy(uri, PullDestination::Store(STORE_ROOT.clone()), None, None)
.await
.expect("failed pulling module");
}

// Create the webassembly module symlink in the "all modules" root
// TODO(ereslibre): figure out Windows behavior
std::os::unix::fs::symlink(
&module_store_path,
all_modules_store_root.join(&module_name),
ALL_MODULES_STORE_ROOT.join(&module_name),
)
.expect("error symlinking top level module");

Expand Down
59 changes: 59 additions & 0 deletions src/rm.rs
@@ -0,0 +1,59 @@
use crate::store::STORE_ROOT;

use std::path::PathBuf;

// This removes the module from the store, and then removes both
// links, the `all` toplevel link of the module itself, and the
// kubectl-plugin link to `krew-wasm`. When the module is removed from
// the store, it also cleans up the structure up to the root of the
// store, so no empty folders are kept around in the store
pub(crate) fn rm(module: &str) {
let (module_paths, module_store_path) =
crate::store::all_module_paths(module).expect("failed to get module paths for module");

// Unlink files that can be directly removed without any extra
// cleanup: the toplevel "all" module and the symlink for the
// kubectl-plugin
for path in module_paths {
#[allow(unused_must_use)]
{
std::fs::remove_file(path);
}
}

if !module_store_path.starts_with(STORE_ROOT.as_path()) {
// Nothing to clean in the store itself, given this module
// comes from another part of the filesystem. Just return.
return;
}

#[allow(unused_must_use)]
{
std::fs::remove_file(&module_store_path);
}

// Clean up parent directories in the store up to its root
{
let mut prefix = STORE_ROOT.clone();
let module_leading_store_components = module_store_path
.iter()
.map(|component| {
prefix = prefix.join(component);
prefix.clone()
})
.collect::<Vec<PathBuf>>();

module_leading_store_components
.iter()
.rev()
.skip(1) // module file -- already unlinked
.take(module_store_path.components().count() - STORE_ROOT.components().count() - 1 /* krew-wasm-store */)
.for_each(|component| {
#[allow(unused_must_use)]
{
// try to clean up empty dirs. Ignore errors.
std::fs::remove_dir(component);
}
})
}
}
45 changes: 45 additions & 0 deletions src/store.rs
@@ -0,0 +1,45 @@
use anyhow::Result;
use directories::{ProjectDirs, UserDirs};
use lazy_static::lazy_static;
use std::path::PathBuf;

lazy_static! {
pub(crate) static ref BIN_ROOT: PathBuf = UserDirs::new()
.expect("cannot find home directory for user")
.home_dir()
.join(".krew-wasm")
.join("bin");
pub(crate) static ref STORE_ROOT: PathBuf = ProjectDirs::from("io.krew-wasm", "", "krew-wasm")
.expect("cannot find project dirs")
.cache_dir()
.join("krew-wasm-store");
pub(crate) static ref ALL_MODULES_STORE_ROOT: PathBuf = STORE_ROOT.join("all");
}

// Given a module name, return a tuple with elements that can be
// unlinked directly in the first component of the tuple, and a second
// argument with the full path to the location in the store. In order
// to leave nothing behind in the store, we need to clean up every
// directory until the root of the store after unlinking the module
// from the store.
pub(crate) fn all_module_paths(module_name: &str) -> Result<(Vec<PathBuf>, PathBuf)> {
let module_bin = BIN_ROOT.join(format!("kubectl-{}", module_name));
let module_root = ALL_MODULES_STORE_ROOT.join(module_name);
let module_path = std::fs::read_link(&module_root)?;
Ok((vec![module_bin, module_root], module_path))
}

pub(crate) fn ensure() {
// Try to create the kubectl plugin bin path.
std::fs::create_dir_all(BIN_ROOT.as_path()).unwrap_or_else(|err| {
panic!(
"could not create alias binary root at {}: {}",
BIN_ROOT.display(),
err
)
});
// Try to create the "all modules" root on the store. Used
// to look for modules given a name.
std::fs::create_dir_all(ALL_MODULES_STORE_ROOT.as_path())
.expect("could not create top level store path for all modules");
}

0 comments on commit 8d1b9f0

Please sign in to comment.