Add bust-cache command
Diff
CHANGELOG.md | 1 +
src/main.rs | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
src/command/bust_cache.rs | 45 +++++++++++++++++++++++++++++++++++++++++++++
src/command/mod.rs | 2 ++
src/git_command_handlers/fetch.rs | 47 -----------------------------------------------
src/git_command_handlers/ls_refs.rs | 31 -------------------------------
src/git_command_handlers/mod.rs | 2 --
src/providers/gitlab.rs | 83 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/providers/mod.rs | 9 +++++++++
src/command/git_upload_pack/fetch.rs | 47 +++++++++++++++++++++++++++++++++++++++++++++++
src/command/git_upload_pack/ls_refs.rs | 31 +++++++++++++++++++++++++++++++
src/command/git_upload_pack/mod.rs | 2 ++
12 files changed, 267 insertions(+), 112 deletions(-)
@@ -9,6 +9,7 @@
- Added crate eligibility cache.
- Introduce configurable cache backend with a RocksDB implementation (set `cache.type = "rocksdb"` and `cache.path = "cache"` to use it), defaults to `cache.type = "in-memory"`.
- Support crate yanking by creating a `yanked` file on the release.
- Add `bust-cache` command, invoked via `ssh [registry] -- bust-cache [project] [crate-name] [crate-version]` to remove eligibility cache (ie. after a crate has been yanked)
# v0.1.4
@@ -6,8 +6,8 @@
)]
pub mod cache;
pub mod command;
pub mod config;
pub mod git_command_handlers;
pub mod metadata;
pub mod providers;
pub mod util;
@@ -19,7 +19,7 @@
providers::{gitlab::Gitlab, PackageProvider, Release, User, UserProvider},
util::get_crate_folder,
};
use anyhow::anyhow;
use anyhow::{anyhow, bail};
use bytes::{BufMut, Bytes, BytesMut};
use clap::Parser;
use futures::{stream::FuturesOrdered, Future, StreamExt};
@@ -485,7 +485,7 @@
match frame.command.as_ref() {
b"command=ls-refs" => {
git_command_handlers::ls_refs::handle(
command::git_upload_pack::ls_refs::handle(
&mut self,
&mut session,
channel,
@@ -494,7 +494,7 @@
)?;
}
b"command=fetch" => {
git_command_handlers::fetch::handle(
command::git_upload_pack::fetch::handle(
&mut self,
&mut session,
channel,
@@ -583,40 +583,55 @@
let mut args = args.into_iter().flat_map(Vec::into_iter);
if args.next().as_deref() != Some("git-upload-pack") {
anyhow::bail!("not git-upload-pack");
}
match args.next().as_deref() {
Some("git-upload-pack") => {
if args.next().as_deref() != Some("git-upload-pack") {
anyhow::bail!("not git-upload-pack");
}
let arg = args.next();
if let Some(project) = arg.as_deref()
.filter(|v| *v != "/")
.map(|project| project.trim_start_matches('/').trim_end_matches('/'))
.filter(|project| project.contains('/'))
{
self.project = Some(Arc::from(project.to_string()));
} else {
session.extended_data(channel, 1, CryptoVec::from_slice(indoc::indoc! {b"
let arg = args.next();
if let Some(project) = arg.as_deref()
.filter(|v| *v != "/")
.map(|project| project.trim_start_matches('/').trim_end_matches('/'))
.filter(|project| project.contains('/'))
{
self.project = Some(Arc::from(project.to_string()));
} else {
session.extended_data(channel, 1, CryptoVec::from_slice(indoc::indoc! {b"
\r\nNo project was given in the path part of the SSH URI. A GitLab group and project should be defined in your .cargo/config.toml as follows:
[registries]
my-project = {{ index = \"ssh://domain.to.registry.com/my-group/my-project\" }}\r\n
"}));
session.close(channel);
}
session.close(channel);
}
self.write(PktLine::Data(b"version 2\n"))?;
self.write(PktLine::Data(AGENT.as_bytes()))?;
self.write(PktLine::Data(b"ls-refs=unborn\n"))?;
self.write(PktLine::Data(b"fetch=shallow wait-for-done\n"))?;
self.write(PktLine::Data(b"server-option\n"))?;
self.write(PktLine::Data(b"object-info\n"))?;
self.write(PktLine::Flush)?;
self.flush(&mut session, channel);
self.write(PktLine::Data(b"version 2\n"))?;
self.write(PktLine::Data(AGENT.as_bytes()))?;
self.write(PktLine::Data(b"ls-refs=unborn\n"))?;
self.write(PktLine::Data(b"fetch=shallow wait-for-done\n"))?;
self.write(PktLine::Data(b"server-option\n"))?;
self.write(PktLine::Data(b"object-info\n"))?;
self.write(PktLine::Flush)?;
self.flush(&mut session, channel);
}
Some("bust-cache") => {
if let Err(e) = command::bust_cache::handle(&mut self, &mut session, channel, args).await {
session.data(
channel,
CryptoVec::from(e.to_string()),
);
session.exit_status_request(channel, 1);
session.close(channel);
}
}
_ => bail!("invalid command"),
}
Ok((self, session))
}).instrument(span))
@@ -1,0 +1,45 @@
use crate::{
providers::{PackageProvider, UserProvider},
Handler,
};
use anyhow::{bail, Context};
use thrussh::server::Session;
use thrussh::{ChannelId, CryptoVec};
use tracing::instrument;
#[instrument(skip_all, err)]
pub async fn handle<U: UserProvider + PackageProvider + Send + Sync + 'static>(
handle: &mut Handler<U>,
session: &mut Session,
channel: ChannelId,
mut params: impl Iterator<Item = String>,
) -> Result<(), anyhow::Error> {
let (Some(project), Some(crate_name), Some(version)) =
(params.next(), params.next(), params.next())
else {
bail!("usage: bust-cache [gitlab project] [crate name] [version]");
};
if !handle
.gitlab
.is_project_maintainer(handle.user()?, &project)
.await
.context("Failed to check project maintainer status")?
{
bail!("This command can only be ran by project maintainers");
}
handle
.gitlab
.bust_cache(&project, &crate_name, &version)
.await?;
session.data(
channel,
CryptoVec::from_slice("Successfully bust cache for release.".as_bytes()),
);
session.exit_status_request(channel, 0);
session.close(channel);
Ok(())
}
@@ -1,0 +1,2 @@
pub mod bust_cache;
pub mod git_upload_pack;
@@ -1,47 +1,0 @@
use bytes::Bytes;
use packfile::{
low_level::{PackFile, PackFileEntry},
PktLine,
};
use thrussh::{server::Session, ChannelId};
use tracing::instrument;
use crate::{Handler, PackageProvider, UserProvider};
#[instrument(skip(handle, session, channel, metadata, packfile_entries), err)]
pub fn handle<U: UserProvider + PackageProvider + Send + Sync + 'static>(
handle: &mut Handler<U>,
session: &mut Session,
channel: ChannelId,
metadata: &[Bytes],
packfile_entries: &[PackFileEntry],
) -> Result<(), anyhow::Error> {
let done = metadata.iter().any(|v| v.as_ref() == b"done");
if !done {
handle.write(PktLine::Data(b"acknowledgments\n"))?;
handle.write(PktLine::Data(b"ready\n"))?;
handle.write(PktLine::Delimiter)?;
}
handle.write(PktLine::Data(b"packfile\n"))?;
let packfile = PackFile::new(packfile_entries);
handle.write(PktLine::SidebandData(packfile))?;
handle.write(PktLine::Flush)?;
handle.flush(session, channel);
session.exit_status_request(channel, 0);
session.eof(channel);
session.close(channel);
Ok(())
}
@@ -1,31 +1,0 @@
use bytes::Bytes;
use packfile::{low_level::HashOutput, PktLine};
use thrussh::{server::Session, ChannelId};
use tracing::instrument;
use crate::{Handler, PackageProvider, UserProvider};
#[instrument(skip(handle, session, channel, _metadata, commit_hash), err)]
pub fn handle<U: UserProvider + PackageProvider + Send + Sync + 'static>(
handle: &mut Handler<U>,
session: &mut Session,
channel: ChannelId,
_metadata: &[Bytes],
commit_hash: &HashOutput,
) -> Result<(), anyhow::Error> {
let commit_hash = hex::encode(commit_hash);
handle.write(PktLine::Data(
format!("{commit_hash} HEAD symref-target:refs/heads/master").as_bytes(),
))?;
handle.write(PktLine::Flush)?;
handle.flush(session, channel);
Ok(())
}
@@ -1,2 +1,0 @@
pub mod fetch;
pub mod ls_refs;
@@ -240,6 +240,28 @@
Ok(impersonation_token.token)
}
#[instrument(skip(self), err)]
async fn is_project_maintainer(&self, do_as: &User, project: &str) -> anyhow::Result<bool> {
let uri = self.base_url.join(&format!(
"projects/{}",
utf8_percent_encode(project, NON_ALPHANUMERIC),
))?;
let result: GitlabProject = handle_error(
self.client
.get(uri)
.user_or_admin_token(do_as, &self.admin_token)
.send_retry_429()
.await?,
)
.await?
.json()
.await?;
Ok(result.permissions.access_level() >= GitlabProjectAccess::MAINTAINER_ACCESS_LEVEL)
}
}
#[async_trait]
@@ -318,6 +340,23 @@
.filter_map(|v| async move { v.map(Result::ok).transpose() })
.try_collect()
.await
}
async fn bust_cache(
&self,
project: &str,
crate_name: &str,
crate_version: &str,
) -> anyhow::Result<()> {
self.cache
.remove::<Option<Release<'static>>>(EligibilityCacheKey::new(
project,
crate_name,
crate_version,
))
.await?;
Ok(())
}
#[instrument(skip(self), err)]
@@ -367,7 +406,51 @@
let msg = json.message.or(json.error).unwrap_or(text);
anyhow::bail!("{url}: {status}: {msg}")
}
}
#[derive(Debug, Deserialize)]
pub struct GitlabProject {
pub permissions: GitlabProjectPermissions,
}
#[derive(Debug, Deserialize)]
pub struct GitlabProjectPermissions {
#[serde(default)]
pub project_access: GitlabProjectAccess,
#[serde(default)]
pub group_access: GitlabProjectAccess,
}
impl GitlabProjectPermissions {
#[must_use]
pub fn access_level(&self) -> u8 {
std::cmp::max(
self.project_access.access_level,
self.group_access.access_level,
)
}
}
#[derive(Debug, Deserialize, Default)]
pub struct GitlabProjectAccess {
access_level: u8,
}
impl GitlabProjectAccess {
pub const MAINTAINER_ACCESS_LEVEL: u8 = 40;
}
#[derive(Default, Deserialize)]
@@ -15,6 +15,8 @@
username_password: &str,
) -> anyhow::Result<Option<User>>;
async fn is_project_maintainer(&self, do_as: &User, project: &str) -> anyhow::Result<bool>;
async fn find_user_by_ssh_key(&self, fingerprint: &str) -> anyhow::Result<Option<User>>;
async fn fetch_token_for_user(&self, user: &User) -> anyhow::Result<String>;
@@ -39,6 +41,13 @@
version: &str,
do_as: &Arc<User>,
) -> anyhow::Result<cargo_metadata::Metadata>;
async fn bust_cache(
&self,
project: &str,
crate_name: &str,
crate_version: &str,
) -> anyhow::Result<()>;
fn cargo_dl_uri(&self, project: &str, token: &str) -> anyhow::Result<String>;
}
@@ -1,0 +1,47 @@
use bytes::Bytes;
use packfile::{
low_level::{PackFile, PackFileEntry},
PktLine,
};
use thrussh::{server::Session, ChannelId};
use tracing::instrument;
use crate::{Handler, PackageProvider, UserProvider};
#[instrument(skip(handle, session, channel, metadata, packfile_entries), err)]
pub fn handle<U: UserProvider + PackageProvider + Send + Sync + 'static>(
handle: &mut Handler<U>,
session: &mut Session,
channel: ChannelId,
metadata: &[Bytes],
packfile_entries: &[PackFileEntry],
) -> Result<(), anyhow::Error> {
let done = metadata.iter().any(|v| v.as_ref() == b"done");
if !done {
handle.write(PktLine::Data(b"acknowledgments\n"))?;
handle.write(PktLine::Data(b"ready\n"))?;
handle.write(PktLine::Delimiter)?;
}
handle.write(PktLine::Data(b"packfile\n"))?;
let packfile = PackFile::new(packfile_entries);
handle.write(PktLine::SidebandData(packfile))?;
handle.write(PktLine::Flush)?;
handle.flush(session, channel);
session.exit_status_request(channel, 0);
session.eof(channel);
session.close(channel);
Ok(())
}
@@ -1,0 +1,31 @@
use bytes::Bytes;
use packfile::{low_level::HashOutput, PktLine};
use thrussh::{server::Session, ChannelId};
use tracing::instrument;
use crate::{Handler, PackageProvider, UserProvider};
#[instrument(skip(handle, session, channel, _metadata, commit_hash), err)]
pub fn handle<U: UserProvider + PackageProvider + Send + Sync + 'static>(
handle: &mut Handler<U>,
session: &mut Session,
channel: ChannelId,
_metadata: &[Bytes],
commit_hash: &HashOutput,
) -> Result<(), anyhow::Error> {
let commit_hash = hex::encode(commit_hash);
handle.write(PktLine::Data(
format!("{commit_hash} HEAD symref-target:refs/heads/master").as_bytes(),
))?;
handle.write(PktLine::Flush)?;
handle.flush(session, channel);
Ok(())
}
@@ -1,0 +1,2 @@
pub mod fetch;
pub mod ls_refs;