Skip to content

Commit 1e9e091

Browse files
committed
Add username/password for Mongo login
1 parent 3f9e037 commit 1e9e091

File tree

8 files changed

+177
-36
lines changed

8 files changed

+177
-36
lines changed

.github/workflows/native-bazel.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,5 @@ jobs:
5959
exit 1
6060
fi
6161
shell: bash
62+
env:
63+
NATIVELINK_TEST_MONGO_URL: ${{ secrets.NATIVELINK_TEST_MONGO_URL}}

.github/workflows/native-cargo.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,5 @@ jobs:
4646

4747
- name: Test on ${{ runner.os }}
4848
run: cargo test --all --profile=smol
49+
env:
50+
NATIVELINK_TEST_MONGO_URL: ${{ secrets.NATIVELINK_TEST_MONGO_URL}}

.github/workflows/nix.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ jobs:
7070
run: >
7171
nix develop --impure --command
7272
bash -c "cargo test --all --profile=smol"
73+
env:
74+
NATIVELINK_TEST_MONGO_URL: ${{ secrets.NATIVELINK_TEST_MONGO_URL }}
7375

7476
installation:
7577
strategy:

.github/workflows/sanitizers.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,5 @@ jobs:
4444
- name: Run Bazel tests
4545
run: bazel test --config=${{ matrix.sanitizer }} --verbose_failures //...
4646
shell: bash
47+
env:
48+
NATIVELINK_TEST_MONGO_URL: ${{ secrets.NATIVELINK_TEST_MONGO_URL}}

nativelink-config/src/stores.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1174,6 +1174,16 @@ pub struct ExperimentalMongoSpec {
11741174
#[serde(deserialize_with = "convert_string_with_shellexpand")]
11751175
pub connection_string: String,
11761176

1177+
// Username to use for connection
1178+
// Default: not set, or use the one from connection_string
1179+
#[serde(default)]
1180+
pub username: String,
1181+
1182+
// Password to use for connection
1183+
// Default: not set, or use the one from connection_string
1184+
#[serde(default)]
1185+
pub password: String,
1186+
11771187
/// The database name to use.
11781188
/// Default: "nativelink"
11791189
#[serde(default, deserialize_with = "convert_string_with_shellexpand")]

nativelink-store/BUILD.bazel

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,11 @@ rust_test_suite(
110110
"tests/size_partitioning_store_test.rs",
111111
"tests/verify_store_test.rs",
112112
],
113+
env_inherit = [
114+
"NATIVELINK_TEST_MONGO_URL",
115+
"CI",
116+
"GITHUB_ACTIONS",
117+
],
113118
proc_macro_deps = [
114119
"//nativelink-macro",
115120
"@crates//:async-trait",

nativelink-store/src/mongo_store.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ use async_trait::async_trait;
2323
use bytes::Bytes;
2424
use futures::stream::{Stream, StreamExt, TryStreamExt};
2525
use mongodb::bson::{Bson, Document, doc};
26-
use mongodb::options::{ClientOptions, FindOptions, IndexOptions, ReturnDocument, WriteConcern};
26+
use mongodb::options::{
27+
ClientOptions, Credential, FindOptions, IndexOptions, ReturnDocument, WriteConcern,
28+
};
2729
use mongodb::{Client as MongoClient, Collection, Database, IndexModel};
2830
use nativelink_config::stores::ExperimentalMongoSpec;
2931
use nativelink_error::{Code, Error, make_err, make_input_err};
@@ -163,6 +165,21 @@ impl ExperimentalMongoStore {
163165
)
164166
})?;
165167

168+
if !spec.username.is_empty() || !spec.password.is_empty() {
169+
if client_options.credential.is_some() {
170+
return Err(make_err!(
171+
Code::InvalidArgument,
172+
"Can't set credentials in both the connection_string and username/password"
173+
));
174+
}
175+
client_options.credential = Some(
176+
Credential::builder()
177+
.username(spec.username)
178+
.password(spec.password)
179+
.build(),
180+
);
181+
}
182+
166183
client_options.server_selection_timeout =
167184
Some(Duration::from_millis(spec.connection_timeout_ms));
168185
client_options.connect_timeout = Some(Duration::from_millis(spec.connection_timeout_ms));

nativelink-store/tests/mongo_store_test.rs

Lines changed: 136 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ fn create_test_spec_with_key_prefix(key_prefix: Option<String>) -> ExperimentalM
5353

5454
ExperimentalMongoSpec {
5555
connection_string: std::env::var("NATIVELINK_TEST_MONGO_URL").unwrap_or(default_connection),
56+
username: String::new(),
57+
password: String::new(),
5658
database: format!(
5759
"nltest_{}_{}",
5860
timestamp,
@@ -83,11 +85,13 @@ pub struct TestMongoHelper {
8385
pub database_name: String,
8486
}
8587

88+
fn in_ci() -> bool {
89+
std::env::var("CI").is_ok() || std::env::var("GITHUB_ACTIONS").is_ok()
90+
}
91+
8692
fn non_ci_test_store_access() {
8793
// Check if this is a local development environment without credentials
88-
let is_ci = std::env::var("CI").is_ok() || std::env::var("GITHUB_ACTIONS").is_ok();
89-
90-
if !is_ci {
94+
if !in_ci() {
9195
eprintln!("\n🔒 MongoDB tests require access to the NativeLink test database.");
9296
eprintln!(" For local development access, please email: [email protected]");
9397
eprintln!(" ");
@@ -177,18 +181,68 @@ async fn create_test_store() -> Result<Arc<ExperimentalMongoStore>, Error> {
177181
ExperimentalMongoStore::new(spec).await
178182
}
179183

184+
enum MongoDBState {
185+
Ok(TestMongoHelper),
186+
Broken(Error),
187+
Skipping,
188+
}
189+
180190
/// Utility to check if `MongoDB` is available for testing.
181191
/// Returns an error that can be used to skip tests when `MongoDB` is not available.
182-
async fn check_mongodb_available() -> Result<(), Error> {
183-
TestMongoHelper::new_or_skip().await.map(|_| ())
192+
async fn check_mongodb_available() -> MongoDBState {
193+
match TestMongoHelper::new_or_skip().await {
194+
Ok(helper) => MongoDBState::Ok(helper),
195+
Err(err) if err.code == Code::Unavailable && !in_ci() => {
196+
eprintln!("Skipping MongoDB test - MongoDB not available");
197+
MongoDBState::Skipping
198+
}
199+
Err(err) => {
200+
eprintln!("Mongo test error: {err}");
201+
MongoDBState::Broken(err)
202+
}
203+
}
204+
}
205+
206+
#[nativelink_test]
207+
async fn connect_with_username_and_password() -> Result<(), Error> {
208+
match check_mongodb_available().await {
209+
MongoDBState::Ok(_helper) => {}
210+
MongoDBState::Broken(err) => {
211+
return Err(err);
212+
}
213+
MongoDBState::Skipping => {
214+
return Ok(());
215+
}
216+
}
217+
218+
let mut spec = create_test_spec();
219+
// These are incorrect, but should allow us to connect and fail with bad login
220+
// v.s. "cannot do login at all" or "config failure"
221+
spec.username = "foo".to_string();
222+
spec.password = "bar".to_string();
223+
let store = ExperimentalMongoStore::new(spec)
224+
.await
225+
.expect("Working store");
226+
let digest = DigestInfo::try_new(VALID_HASH1, 2)?;
227+
let result = store.has(digest).await?;
228+
assert!(
229+
result.is_some(),
230+
"Expected mongo store to have hash: {VALID_HASH1}",
231+
);
232+
Ok(())
184233
}
185234

186235
#[nativelink_test]
187236
async fn upload_and_get_data() -> Result<(), Error> {
188237
// Create test helper with automatic cleanup
189-
let Ok(helper) = TestMongoHelper::new_or_skip().await else {
190-
eprintln!("Skipping MongoDB test - MongoDB not available");
191-
return Ok(());
238+
let helper = match check_mongodb_available().await {
239+
MongoDBState::Ok(helper) => helper,
240+
MongoDBState::Broken(err) => {
241+
return Err(err);
242+
}
243+
MongoDBState::Skipping => {
244+
return Ok(());
245+
}
192246
};
193247

194248
// Construct the data we want to send.
@@ -252,9 +306,14 @@ async fn upload_and_get_data_without_prefix() -> Result<(), Error> {
252306
#[nativelink_test]
253307
async fn upload_empty_data() -> Result<(), Error> {
254308
// Skip test if MongoDB is not available
255-
if check_mongodb_available().await.is_err() {
256-
eprintln!("Skipping MongoDB test - MongoDB not available");
257-
return Ok(());
309+
match check_mongodb_available().await {
310+
MongoDBState::Ok(_helper) => {}
311+
MongoDBState::Broken(err) => {
312+
return Err(err);
313+
}
314+
MongoDBState::Skipping => {
315+
return Ok(());
316+
}
258317
}
259318

260319
let data = Bytes::from_static(b"");
@@ -277,9 +336,14 @@ async fn upload_empty_data() -> Result<(), Error> {
277336
#[allow(clippy::items_after_statements)]
278337
async fn test_large_downloads_are_chunked() -> Result<(), Error> {
279338
// Skip test if MongoDB is not available
280-
if check_mongodb_available().await.is_err() {
281-
eprintln!("Skipping MongoDB test - MongoDB not available");
282-
return Ok(());
339+
match check_mongodb_available().await {
340+
MongoDBState::Ok(_helper) => {}
341+
MongoDBState::Broken(err) => {
342+
return Err(err);
343+
}
344+
MongoDBState::Skipping => {
345+
return Ok(());
346+
}
283347
}
284348

285349
const READ_CHUNK_SIZE: usize = 1024;
@@ -316,9 +380,14 @@ async fn test_large_downloads_are_chunked() -> Result<(), Error> {
316380
#[allow(clippy::items_after_statements)]
317381
async fn yield_between_sending_packets_in_update() -> Result<(), Error> {
318382
// Skip test if MongoDB is not available
319-
if check_mongodb_available().await.is_err() {
320-
eprintln!("Skipping MongoDB test - MongoDB not available");
321-
return Ok(());
383+
match check_mongodb_available().await {
384+
MongoDBState::Ok(_helper) => {}
385+
MongoDBState::Broken(err) => {
386+
return Err(err);
387+
}
388+
MongoDBState::Skipping => {
389+
return Ok(());
390+
}
322391
}
323392

324393
const READ_CHUNK_SIZE: usize = 1024;
@@ -372,9 +441,14 @@ async fn yield_between_sending_packets_in_update() -> Result<(), Error> {
372441
#[nativelink_test]
373442
async fn zero_len_items_exist_check() -> Result<(), Error> {
374443
// Skip test if MongoDB is not available
375-
if check_mongodb_available().await.is_err() {
376-
eprintln!("Skipping MongoDB test - MongoDB not available");
377-
return Ok(());
444+
match check_mongodb_available().await {
445+
MongoDBState::Ok(_helper) => {}
446+
MongoDBState::Broken(err) => {
447+
return Err(err);
448+
}
449+
MongoDBState::Skipping => {
450+
return Ok(());
451+
}
378452
}
379453

380454
let digest = DigestInfo::try_new(VALID_HASH1, 0)?;
@@ -427,9 +501,14 @@ async fn test_empty_connection_string() -> Result<(), Error> {
427501
#[nativelink_test]
428502
async fn test_default_values() -> Result<(), Error> {
429503
// Skip test if MongoDB is not available
430-
if check_mongodb_available().await.is_err() {
431-
eprintln!("Skipping MongoDB test - MongoDB not available");
432-
return Ok(());
504+
match check_mongodb_available().await {
505+
MongoDBState::Ok(_helper) => {}
506+
MongoDBState::Broken(err) => {
507+
return Err(err);
508+
}
509+
MongoDBState::Skipping => {
510+
return Ok(());
511+
}
433512
}
434513

435514
let mut spec = create_test_spec();
@@ -464,9 +543,14 @@ async fn test_default_values() -> Result<(), Error> {
464543
#[nativelink_test]
465544
async fn dont_loop_forever_on_empty() -> Result<(), Error> {
466545
// Skip test if MongoDB is not available
467-
if check_mongodb_available().await.is_err() {
468-
eprintln!("Skipping MongoDB test - MongoDB not available");
469-
return Ok(());
546+
match check_mongodb_available().await {
547+
MongoDBState::Ok(_helper) => {}
548+
MongoDBState::Broken(err) => {
549+
return Err(err);
550+
}
551+
MongoDBState::Skipping => {
552+
return Ok(());
553+
}
470554
}
471555

472556
let store = create_test_store().await?;
@@ -502,9 +586,14 @@ async fn dont_loop_forever_on_empty() -> Result<(), Error> {
502586
#[nativelink_test]
503587
async fn test_partial_reads() -> Result<(), Error> {
504588
// Create test helper with automatic cleanup
505-
let Ok(helper) = TestMongoHelper::new_or_skip().await else {
506-
eprintln!("Skipping MongoDB test - MongoDB not available");
507-
return Ok(());
589+
let helper = match check_mongodb_available().await {
590+
MongoDBState::Ok(helper) => helper,
591+
MongoDBState::Broken(err) => {
592+
return Err(err);
593+
}
594+
MongoDBState::Skipping => {
595+
return Ok(());
596+
}
508597
};
509598

510599
let data = Bytes::from_static(b"Hello, MongoDB World!");
@@ -620,9 +709,14 @@ async fn test_database_lifecycle() -> Result<(), Error> {
620709
#[allow(clippy::use_debug)]
621710
async fn create_ten_cas_entries() -> Result<(), Error> {
622711
// Create test helper with automatic cleanup
623-
let Ok(helper) = TestMongoHelper::new_or_skip().await else {
624-
eprintln!("Skipping MongoDB test - MongoDB not available");
625-
return Ok(());
712+
let helper = match check_mongodb_available().await {
713+
MongoDBState::Ok(helper) => helper,
714+
MongoDBState::Broken(err) => {
715+
return Err(err);
716+
}
717+
MongoDBState::Skipping => {
718+
return Ok(());
719+
}
626720
};
627721

628722
eprintln!(
@@ -848,9 +942,14 @@ impl SchedulerStoreDecodeTo for TestSchedulerKey {
848942
#[nativelink_test]
849943
async fn test_scheduler_store_operations() -> Result<(), Error> {
850944
// Create test helper
851-
let Ok(helper) = TestMongoHelper::new_or_skip().await else {
852-
eprintln!("Skipping MongoDB test - MongoDB not available");
853-
return Ok(());
945+
let helper = match check_mongodb_available().await {
946+
MongoDBState::Ok(helper) => helper,
947+
MongoDBState::Broken(err) => {
948+
return Err(err);
949+
}
950+
MongoDBState::Skipping => {
951+
return Ok(());
952+
}
854953
};
855954

856955
eprintln!(
@@ -1062,6 +1161,8 @@ async fn test_non_w_config() -> Result<(), Error> {
10621161
Error::new(Code::InvalidArgument, "write_concern_w not set, but j and/or timeout set. Please set 'write_concern_w' to a non-default value. See https://www.mongodb.com/docs/manual/reference/write-concern/#w-option for options.".to_string()),
10631162
ExperimentalMongoStore::new(ExperimentalMongoSpec {
10641163
connection_string: "mongodb://dummy".to_string(),
1164+
username: String::new(),
1165+
password: String::new(),
10651166
database: "dummy".to_string(),
10661167
cas_collection: "test_cas".to_string(),
10671168
scheduler_collection: "test_scheduler".to_string(),

0 commit comments

Comments
 (0)