Skip to content

Commit 090c2b7

Browse files
lmvasquezgmeta-codesync[bot]
authored andcommitted
Add restricted-paths admin command to list and delete manifest-id entries
Summary: Cleaning up content-hash collisions in the ACL Manifest Store today means running ad-hoc SQL DELETEs against the production `restricted_paths_manifest_ids` XDB table. That is error-prone: a hand-written DELETE has no dry-run or confirmation step before it mutates prod, and the binary `manifest_id` column makes it easy to pass the wrong value and silently match nothing. This adds a `mononoke-admin restricted-paths` command with `manifest-id-store list` and `manifest-id-store delete` subcommands that look up and remove entries by `manifest_id` for the repo selected on the command line, building on the store methods added in the parent commit. The subcommands are grouped under `manifest-id-store` so future restricted-paths tooling (backfills, path lookups, etc.) can be added alongside without crowding the top level. `delete` first prints the affected rows and prompts for confirmation (with `--force` to skip), so the operator sees exactly what will be removed before anything is deleted, and it warns if the number of rows actually deleted differs from the preview. The `--manifest-id` argument accepts the hash with or without a `0x` prefix and rejects empty or malformed hex up front rather than silently matching nothing. A colliding manifest id that spans several repos is cleaned up by running the command once per repo. An integration test (`test-admin-restricted-paths.t`) covers listing, deletion, the empty-result paths, and invalid/empty manifest-id handling. Reviewed By: gustavoavena Differential Revision: D110107711 fbshipit-source-id: 0fe371ab0e468371fffff54207c88cae7f814ee0
1 parent eef1781 commit 090c2b7

6 files changed

Lines changed: 220 additions & 0 deletions

File tree

‎eden/mononoke/tools/admin/BUCK‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,14 @@ rust_binary(
5151
"fbsource//third-party/rust:futures",
5252
"fbsource//third-party/rust:gix-hash",
5353
"fbsource//third-party/rust:gix-object",
54+
"fbsource//third-party/rust:hex",
5455
"fbsource//third-party/rust:itertools",
5556
"fbsource//third-party/rust:maplit",
5657
"fbsource//third-party/rust:num-format",
5758
"fbsource//third-party/rust:prettytable-rs",
5859
"fbsource//third-party/rust:regex",
5960
"fbsource//third-party/rust:serde_json",
61+
"fbsource//third-party/rust:smallvec",
6062
"fbsource//third-party/rust:strum",
6163
"fbsource//third-party/rust:thiserror",
6264
"fbsource//third-party/rust:tokio",
@@ -166,6 +168,7 @@ rust_binary(
166168
"//eden/mononoke/repo_attributes/repo_derived_data:repo_derived_data",
167169
"//eden/mononoke/repo_attributes/repo_identity:repo_identity",
168170
"//eden/mononoke/repo_attributes/repo_lock:repo_lock",
171+
"//eden/mononoke/repo_attributes/restricted_paths:restricted_paths",
169172
"//eden/mononoke/repo_attributes/sql_query_config:sql_query_config",
170173
"//eden/mononoke/repo_factory:repo_factory",
171174
"//eden/mononoke/scs/if:source_control-rust",

‎eden/mononoke/tools/admin/Cargo.toml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ git_symbolic_refs = { version = "0.1.0", path = "../../repo_attributes/git_symbo
7575
git_types = { version = "0.1.0", path = "../../git/git_types" }
7676
gix-hash = { version = "0.25.1", features = ["sha1"] }
7777
gix-object = "0.60.0"
78+
hex = { version = "0.4.3", features = ["alloc"] }
7879
history_manifest = { version = "0.1.0", path = "../../derived_data/history_manifest" }
7980
itertools = "0.15.0"
8081
justknobs_ext = { version = "0.1.0", path = "../../common/rust/justknobs_ext" }
@@ -122,6 +123,7 @@ repo_identity = { version = "0.1.0", path = "../../repo_attributes/repo_identity
122123
repo_lock = { version = "0.1.0", path = "../../repo_attributes/repo_lock/repo_lock" }
123124
repo_update_logger = { version = "0.1.0", path = "../../features/repo_update_logger" }
124125
requests_table = { version = "0.1.0", path = "../../features/async_requests/requests_table" }
126+
restricted_paths = { version = "0.1.0", path = "../../repo_attributes/restricted_paths" }
125127
sapling-dag = { version = "0.1.0", path = "../../../scm/lib/dag" }
126128
sapling-dag-types = { version = "0.1.0", path = "../../../scm/lib/dag/dag-types", features = ["for-tests"] }
127129
sapling-renderdag = { version = "0.1.0", path = "../../../scm/lib/renderdag" }
@@ -130,6 +132,7 @@ serde = { version = "1.0.219", features = ["derive", "rc"] }
130132
serde_json = { version = "1.0.140", features = ["alloc", "float_roundtrip", "raw_value", "unbounded_depth"] }
131133
skeleton_manifest = { version = "0.1.0", path = "../../derived_data/skeleton_manifest" }
132134
skeleton_manifest_v2 = { version = "0.1.0", path = "../../derived_data/skeleton_manifest_v2" }
135+
smallvec = { version = "1.15.2", features = ["impl_bincode", "serde", "specialization", "union", "write"] }
133136
sorted_vector_map = { version = "0.2.1", git = "https://github.com/facebookexperimental/rust-shed.git", branch = "main" }
134137
source_control = { version = "0.1.0", path = "../../scs/if" }
135138
sql_commit_graph_storage = { version = "0.1.0", path = "../../repo_attributes/commit_graph/sql_commit_graph_storage" }

‎eden/mononoke/tools/admin/src/commands.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,5 +60,6 @@ mononoke_app::subcommands! {
6060
mod raw_blobstore;
6161
mod redaction;
6262
mod repo_info;
63+
mod restricted_paths;
6364
mod slow_bookmark_mover;
6465
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This software may be used and distributed according to the terms of the
5+
* GNU General Public License version 2.
6+
*/
7+
8+
mod delete;
9+
mod list;
10+
11+
use anyhow::Context;
12+
use anyhow::Result;
13+
use anyhow::bail;
14+
use clap::Parser;
15+
use clap::Subcommand;
16+
use mononoke_app::MononokeApp;
17+
use mononoke_app::args::RepoArgs;
18+
use repo_identity::RepoIdentity;
19+
use restricted_paths::ManifestId;
20+
use restricted_paths::RestrictedPathsManifestIdStore;
21+
use smallvec::SmallVec;
22+
23+
/// Inspect and manage restricted paths state for a repo.
24+
///
25+
/// Subcommands are grouped by the underlying state they operate on (e.g.
26+
/// `manifest-id-store`), leaving room for future restricted-paths tooling
27+
/// (backfills, path lookups, etc.) that is unrelated to the manifest-id store.
28+
#[derive(Parser)]
29+
pub struct CommandArgs {
30+
#[clap(flatten)]
31+
repo: RepoArgs,
32+
33+
#[clap(subcommand)]
34+
subcommand: RestrictedPathsSubcommand,
35+
}
36+
37+
#[facet::container]
38+
pub struct Repo {
39+
#[facet]
40+
restricted_paths_manifest_id_store: dyn RestrictedPathsManifestIdStore,
41+
#[facet]
42+
repo_identity: RepoIdentity,
43+
}
44+
45+
#[derive(Subcommand)]
46+
pub enum RestrictedPathsSubcommand {
47+
/// Inspect and manage the manifest-id store.
48+
ManifestIdStore {
49+
#[clap(subcommand)]
50+
command: ManifestIdStoreSubcommand,
51+
},
52+
}
53+
54+
#[derive(Subcommand)]
55+
pub enum ManifestIdStoreSubcommand {
56+
/// List the entries matching a manifest id in the selected repo.
57+
List(list::ListArgs),
58+
/// Delete the entries matching a manifest id in the selected repo.
59+
Delete(delete::DeleteArgs),
60+
}
61+
62+
/// Parse a manifest id from its hex representation.
63+
///
64+
/// Accepts an optional `0x`/`0X` prefix. The bytes are decoded with strict hex
65+
/// parsing rather than `ManifestId::from(&str)`, whose fallback silently treats
66+
/// invalid hex as raw ASCII bytes and would mask user mistakes.
67+
pub(crate) fn parse_manifest_id(s: &str) -> Result<ManifestId> {
68+
let hex_str = s
69+
.strip_prefix("0x")
70+
.or_else(|| s.strip_prefix("0X"))
71+
.unwrap_or(s);
72+
let bytes = match hex::decode(hex_str) {
73+
Ok(bytes) => bytes,
74+
Err(e) => bail!("Invalid hex manifest_id {s:?}: {e}"),
75+
};
76+
if bytes.is_empty() {
77+
bail!("Empty manifest_id {s:?}: a manifest id must be a non-empty hex string");
78+
}
79+
Ok(ManifestId::new(SmallVec::from_slice(&bytes)))
80+
}
81+
82+
pub async fn run(app: MononokeApp, args: CommandArgs) -> Result<()> {
83+
let ctx = app.new_basic_context();
84+
let repo: Repo = app
85+
.open_repo(&args.repo)
86+
.await
87+
.context("Failed to open repo")?;
88+
89+
match args.subcommand {
90+
RestrictedPathsSubcommand::ManifestIdStore { command } => match command {
91+
ManifestIdStoreSubcommand::List(args) => list::list(&ctx, &repo, args).await?,
92+
ManifestIdStoreSubcommand::Delete(args) => delete::delete(&ctx, &repo, args).await?,
93+
},
94+
}
95+
Ok(())
96+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This software may be used and distributed according to the terms of the
5+
* GNU General Public License version 2.
6+
*/
7+
8+
use std::io::Write;
9+
10+
use anyhow::Result;
11+
use clap::Args;
12+
use context::CoreContext;
13+
use restricted_paths::manifest_id_store::RestrictedPathsManifestIdStoreRef;
14+
15+
use super::Repo;
16+
use super::parse_manifest_id;
17+
18+
#[derive(Args)]
19+
pub struct DeleteArgs {
20+
/// The manifest id to delete, in hex (an optional `0x` prefix is accepted).
21+
#[clap(long)]
22+
manifest_id: String,
23+
/// Skip the interactive confirmation prompt.
24+
#[clap(long)]
25+
force: bool,
26+
}
27+
28+
pub async fn delete(ctx: &CoreContext, repo: &Repo, args: DeleteArgs) -> Result<()> {
29+
let manifest_id = parse_manifest_id(&args.manifest_id)?;
30+
let store = repo.restricted_paths_manifest_id_store();
31+
let entries = store
32+
.get_all_paths_by_manifest_id(ctx, &manifest_id)
33+
.await?;
34+
35+
if entries.is_empty() {
36+
println!("No entries found for manifest_id {manifest_id}; nothing to delete.");
37+
return Ok(());
38+
}
39+
40+
let expected = entries.len();
41+
println!("Found {expected} entries for manifest_id {manifest_id}:");
42+
println!("manifest_type\tpath");
43+
for (manifest_type, path) in &entries {
44+
println!("{manifest_type}\t{path}");
45+
}
46+
47+
if !args.force {
48+
// The prompt reads from stdin, which would block the tokio runtime, so
49+
// run it on the blocking pool.
50+
let confirmed = tokio::task::spawn_blocking(move || -> Result<bool> {
51+
print!(
52+
"Are you sure you want to delete {expected} manifest entries from the DB? [y/N]: "
53+
);
54+
std::io::stdout().flush()?;
55+
56+
let mut input = String::new();
57+
std::io::stdin().read_line(&mut input)?;
58+
let answer = input.trim().to_lowercase();
59+
Ok(answer == "y" || answer == "yes")
60+
})
61+
.await??;
62+
63+
if !confirmed {
64+
println!("Aborted.");
65+
return Ok(());
66+
}
67+
}
68+
69+
let deleted = store.delete_by_manifest_id(ctx, &manifest_id).await?;
70+
println!("Deleted {deleted} rows.");
71+
if deleted != expected as u64 {
72+
println!(
73+
"Warning: deleted {deleted} rows but {expected} were shown in the preview; the table changed between listing and deletion."
74+
);
75+
}
76+
Ok(())
77+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This software may be used and distributed according to the terms of the
5+
* GNU General Public License version 2.
6+
*/
7+
8+
use anyhow::Result;
9+
use clap::Args;
10+
use context::CoreContext;
11+
use restricted_paths::manifest_id_store::RestrictedPathsManifestIdStoreRef;
12+
13+
use super::Repo;
14+
use super::parse_manifest_id;
15+
16+
#[derive(Args)]
17+
pub struct ListArgs {
18+
/// The manifest id to look up, in hex (an optional `0x` prefix is accepted).
19+
#[clap(long)]
20+
manifest_id: String,
21+
}
22+
23+
pub async fn list(ctx: &CoreContext, repo: &Repo, args: ListArgs) -> Result<()> {
24+
let manifest_id = parse_manifest_id(&args.manifest_id)?;
25+
let store = repo.restricted_paths_manifest_id_store();
26+
let entries = store
27+
.get_all_paths_by_manifest_id(ctx, &manifest_id)
28+
.await?;
29+
30+
if entries.is_empty() {
31+
println!("No entries found for manifest_id {manifest_id}");
32+
return Ok(());
33+
}
34+
35+
println!("manifest_type\tpath");
36+
for (manifest_type, path) in entries {
37+
println!("{manifest_type}\t{path}");
38+
}
39+
Ok(())
40+
}

0 commit comments

Comments
 (0)