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

Registry API Command Versioning #6442

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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