Skip to content

Commit

Permalink
Registry API Command Versioning
Browse files Browse the repository at this point in the history
  • Loading branch information
ehuss committed Dec 14, 2018
1 parent 79f962f commit ca49031
Show file tree
Hide file tree
Showing 14 changed files with 332 additions and 128 deletions.
52 changes: 10 additions & 42 deletions src/bin/cargo/commands/login.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
use crate::command_prelude::*;

use std::io::{self, BufRead};

use cargo::core::{Source, SourceId};
use cargo::ops;
use cargo::sources::RegistrySource;
use cargo::util::CargoResultExt;

pub fn cli() -> App {
subcommand("login")
Expand All @@ -14,46 +9,19 @@ pub fn cli() -> App {
If token is not specified, it will be read from stdin.",
)
.arg(Arg::with_name("token"))
.arg(opt("host", "Host to set the token for").value_name("HOST"))
.arg(
opt("host", "Host to set the token for")
.value_name("HOST")
.hidden(true),
)
.arg(opt("registry", "Registry to use").value_name("REGISTRY"))
}

pub fn exec(config: &mut Config, args: &ArgMatches<'_>) -> CliResult {
let registry = args.registry(config)?;

let token = match args.value_of("token") {
Some(token) => token.to_string(),
None => {
let host = match registry {
Some(ref _registry) => {
return Err(failure::format_err!(
"token must be provided when \
--registry is provided."
)
.into());
}
None => {
let src = SourceId::crates_io(config)?;
let mut src = RegistrySource::remote(src, config);
src.update()?;
let config = src.config()?.unwrap();
args.value_of("host")
.map(|s| s.to_string())
.unwrap_or_else(|| config.api.unwrap())
}
};
println!("please visit {}/me and paste the API Token below", host);
let mut line = String::new();
let input = io::stdin();
input
.lock()
.read_line(&mut line)
.chain_err(|| "failed to read stdin")
.map_err(failure::Error::from)?;
line.trim().to_string()
}
};

ops::registry_login(config, token, registry)?;
ops::registry_login(
config,
args.value_of("token").map(String::from),
args.value_of("registry").map(String::from),
)?;
Ok(())
}
1 change: 0 additions & 1 deletion src/bin/cargo/commands/package.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ pub fn exec(config: &mut Config, args: &ArgMatches<'_>) -> CliResult {
allow_dirty: args.is_present("allow-dirty"),
target: args.target(),
jobs: args.jobs()?,
registry: None,
},
)?;
Ok(())
Expand Down
1 change: 0 additions & 1 deletion src/cargo/ops/cargo_package.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ pub struct PackageOpts<'cfg> {
pub verify: bool,
pub jobs: Option<u32>,
pub target: Option<String>,
pub registry: Option<String>,
}

static VCS_INFO_FILE: &'static str = ".cargo_vcs_info.json";
Expand Down
80 changes: 55 additions & 25 deletions src/cargo/ops/registry.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::collections::BTreeMap;
use std::fs::{self, File};
use std::io::{self, BufRead};
use std::iter::repeat;
use std::str;
use std::time::Duration;
Expand Down Expand Up @@ -80,7 +81,6 @@ pub fn publish(ws: &Workspace<'_>, opts: &PublishOpts<'_>) -> CargoResult<()> {
allow_dirty: opts.allow_dirty,
target: opts.target.clone(),
jobs: opts.jobs,
registry: opts.registry.clone(),
},
)?
.unwrap();
Expand Down Expand Up @@ -325,14 +325,25 @@ pub fn registry(
} = registry_configuration(config, registry.clone())?;
let token = token.or(token_config);
let sid = get_source_id(config, index_config.or(index), registry)?;
let api_host = {
let (api_host, commands) = {
let mut src = RegistrySource::remote(sid, config);
src.update()
.chain_err(|| format!("failed to update {}", sid))?;
(src.config()?).unwrap().api.unwrap()
// Only update the index if the config is not available.
let cfg = match src.config() {
Ok(c) => c,
Err(_) => {
src.update()
.chain_err(|| format!("failed to update {}", sid))?;
src.config()?
}
}
.ok_or_else(|| failure::format_err!("{} does not support API commands", sid))?;
let api = cfg
.api
.ok_or_else(|| failure::format_err!("{} does not support API commands", sid))?;
(api, cfg.commands)
};
let handle = http_handle(config)?;
Ok((Registry::new_handle(api_host, token, handle), sid))
Ok((Registry::new_handle(api_host, commands, token, handle), sid))
}

/// Create a new HTTP handle with appropriate global configuration for cargo.
Expand Down Expand Up @@ -500,18 +511,52 @@ fn http_proxy_exists(config: &Config) -> CargoResult<bool> {
}
}

pub fn registry_login(config: &Config, token: String, registry: Option<String>) -> CargoResult<()> {
pub fn registry_login(
config: &Config,
token: Option<String>,
reg: Option<String>,
) -> CargoResult<()> {
let (registry, _) = registry(config, token.clone(), None, reg.clone())?;
registry.supports_command("login", "v1")?;

let token = match token {
Some(token) => token,
None => {
println!(
"please visit {}/me and paste the API Token below",
registry.host()
);
let mut line = String::new();
let input = io::stdin();
input
.lock()
.read_line(&mut line)
.chain_err(|| "failed to read stdin")
.map_err(failure::Error::from)?;
line.trim().to_string()
}
};

let RegistryConfig {
token: old_token, ..
} = registry_configuration(config, registry.clone())?;
} = registry_configuration(config, reg.clone())?;

if let Some(old_token) = old_token {
if old_token == token {
config.shell().status("Login", "already logged in")?;
return Ok(());
}
}

config::save_credentials(config, token, registry)
config::save_credentials(config, token, reg.clone())?;
config.shell().status(
"Login",
format!(
"token for `{}` saved",
reg.as_ref().map_or("crates.io", String::as_str)
),
)?;
Ok(())
}

pub struct OwnersOptions {
Expand Down Expand Up @@ -655,22 +700,7 @@ pub fn search(
prefix
}

let sid = get_source_id(config, index, reg)?;

let mut regsrc = RegistrySource::remote(sid, config);
let cfg = match regsrc.config() {
Ok(c) => c,
Err(_) => {
regsrc
.update()
.chain_err(|| format!("failed to update {}", &sid))?;
regsrc.config()?
}
};

let api_host = cfg.unwrap().api.unwrap();
let handle = http_handle(config)?;
let mut registry = Registry::new_handle(api_host, None, handle);
let (mut registry, _) = registry(config, None, index, reg)?;
let (crates, total_crates) = registry
.search(query, limit)
.chain_err(|| "failed to retrieve search results from the registry")?;
Expand Down
8 changes: 8 additions & 0 deletions src/cargo/sources/registry/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,15 @@ pub struct RegistryConfig {

/// API endpoint for the registry. This is what's actually hit to perform
/// operations like yanks, owner modifications, publish new crates, etc.
/// If this is None, the registry does not support API commands.
pub api: Option<String>,

/// Map of commands the registry supports.
/// Key is the command name ("yank", "publish", etc.) and the value is a
/// list of versions supported for that command (["v1"]).
/// If not specified, but `api` is set, all commands default to v1.
#[serde(default)]
pub commands: BTreeMap<String, Vec<String>>,
}

#[derive(Deserialize)]
Expand Down
12 changes: 11 additions & 1 deletion src/cargo/sources/registry/remote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,17 @@ impl<'cfg> RegistryData for RemoteRegistry<'cfg> {
.open_ro(Path::new(INDEX_LOCK), self.config, "the registry index")?;
let mut config = None;
self.load(Path::new(""), Path::new("config.json"), &mut |json| {
config = Some(serde_json::from_slice(json)?);
let mut reg_conf: RegistryConfig = serde_json::from_slice(json)?;
if reg_conf.api.is_some() && reg_conf.commands.is_empty() {
// Default to V1 if not specified.
let v1 = vec!["v1".to_string()];
reg_conf.commands.insert("publish".to_string(), v1.clone());
reg_conf.commands.insert("yank".to_string(), v1.clone());
reg_conf.commands.insert("search".to_string(), v1.clone());
reg_conf.commands.insert("owner".to_string(), v1.clone());
reg_conf.commands.insert("login".to_string(), v1.clone());
}
config = Some(reg_conf);
Ok(())
})?;
trace!("config loaded");
Expand Down
50 changes: 46 additions & 4 deletions src/crates-io/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,16 @@ use url::percent_encoding::{percent_encode, QUERY_ENCODE_SET};
pub type Result<T> = std::result::Result<T, failure::Error>;

pub struct Registry {
/// The base URL for issuing API requests.
host: String,
/// Map of commands this registry supports.
/// Maps command (ex "yank") to list of versions of the API it supports
/// (ex ["v1"]).
commands: BTreeMap<String, Vec<String>>,
/// Optional authorization token.
/// If None, commands requiring authorization will fail.
token: Option<String>,
/// Curl handle for issuing requests.
handle: Easy,
}

Expand Down Expand Up @@ -51,7 +59,8 @@ pub struct NewCrate {
pub license_file: Option<String>,
pub repository: Option<String>,
pub badges: BTreeMap<String, BTreeMap<String, String>>,
#[serde(default)] pub links: Option<String>,
#[serde(default)]
pub links: Option<String>,
}

#[derive(Serialize)]
Expand Down Expand Up @@ -119,38 +128,56 @@ struct Crates {
meta: TotalCrates,
}
impl Registry {
pub fn new(host: String, token: Option<String>) -> Registry {
Registry::new_handle(host, token, Easy::new())
pub fn new(
host: String,
commands: BTreeMap<String, Vec<String>>,
token: Option<String>,
) -> Registry {
Registry::new_handle(host, commands, token, Easy::new())
}

pub fn new_handle(host: String, token: Option<String>, handle: Easy) -> Registry {
pub fn new_handle(
host: String,
commands: BTreeMap<String, Vec<String>>,
token: Option<String>,
handle: Easy,
) -> Registry {
Registry {
host,
commands,
token,
handle,
}
}

pub fn host(&self) -> &str {
&self.host
}

pub fn add_owners(&mut self, krate: &str, owners: &[&str]) -> Result<String> {
self.supports_command("owner", "v1")?;
let body = serde_json::to_string(&OwnersReq { users: owners })?;
let body = self.put(&format!("/crates/{}/owners", krate), body.as_bytes())?;
assert!(serde_json::from_str::<OwnerResponse>(&body)?.ok);
Ok(serde_json::from_str::<OwnerResponse>(&body)?.msg)
}

pub fn remove_owners(&mut self, krate: &str, owners: &[&str]) -> Result<()> {
self.supports_command("owner", "v1")?;
let body = serde_json::to_string(&OwnersReq { users: owners })?;
let body = self.delete(&format!("/crates/{}/owners", krate), Some(body.as_bytes()))?;
assert!(serde_json::from_str::<OwnerResponse>(&body)?.ok);
Ok(())
}

pub fn list_owners(&mut self, krate: &str) -> Result<Vec<User>> {
self.supports_command("owner", "v1")?;
let body = self.get(&format!("/crates/{}/owners", krate))?;
Ok(serde_json::from_str::<Users>(&body)?.users)
}

pub fn publish(&mut self, krate: &NewCrate, tarball: &File) -> Result<Warnings> {
self.supports_command("publish", "v1")?;
let json = serde_json::to_string(krate)?;
// Prepare the body. The format of the upload request is:
//
Expand Down Expand Up @@ -234,6 +261,7 @@ impl Registry {
}

pub fn search(&mut self, query: &str, limit: u32) -> Result<(Vec<Crate>, u32)> {
self.supports_command("search", "v1")?;
let formatted_query = percent_encode(query.as_bytes(), QUERY_ENCODE_SET);
let body = self.req(
&format!("/crates?q={}&per_page={}", formatted_query, limit),
Expand All @@ -246,17 +274,31 @@ impl Registry {
}

pub fn yank(&mut self, krate: &str, version: &str) -> Result<()> {
self.supports_command("yank", "v1")?;
let body = self.delete(&format!("/crates/{}/{}/yank", krate, version), None)?;
assert!(serde_json::from_str::<R>(&body)?.ok);
Ok(())
}

pub fn unyank(&mut self, krate: &str, version: &str) -> Result<()> {
self.supports_command("yank", "v1")?;
let body = self.put(&format!("/crates/{}/{}/unyank", krate, version), &[])?;
assert!(serde_json::from_str::<R>(&body)?.ok);
Ok(())
}

pub fn supports_command(&self, cmd: &str, vers: &str) -> Result<()> {
if self
.commands
.get(cmd)
.map_or(false, |vs| vs.iter().any(|v| v == vers))
{
Ok(())
} else {
bail!("registry does not support command: {} {}", cmd, vers)
}
}

fn put(&mut self, path: &str, b: &[u8]) -> Result<String> {
self.handle.put(true)?;
self.req(path, Some(b), Auth::Authorized)
Expand Down
Loading

0 comments on commit ca49031

Please sign in to comment.