Skip to content
Open
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
1 change: 1 addition & 0 deletions core/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions core/services/swift/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ all-features = true
bytes = { workspace = true }
http = { workspace = true }
log = { workspace = true }
mea = { workspace = true }
opendal-core = { path = "../../core", version = "0.55.0", default-features = false }
quick-xml = { workspace = true, features = ["serialize", "overlapped-lists"] }
serde = { workspace = true, features = ["derive"] }
Expand Down
254 changes: 204 additions & 50 deletions core/services/swift/src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use std::sync::Arc;
use http::Response;
use http::StatusCode;
use log::debug;
use mea::mutex::Mutex;

use super::SWIFT_SCHEME;
use super::SwiftConfig;
Expand Down Expand Up @@ -52,6 +53,9 @@ impl SwiftBuilder {
///
/// If user inputs endpoint without scheme, we will
/// prepend `https://` to it.
///
/// When using Keystone v3 authentication, the endpoint can be omitted
/// and will be discovered from the service catalog.
pub fn endpoint(mut self, endpoint: &str) -> Self {
self.config.endpoint = if endpoint.is_empty() {
None
Expand Down Expand Up @@ -95,6 +99,60 @@ impl SwiftBuilder {
}
self
}

/// Set the Keystone v3 authentication URL.
///
/// e.g. `https://keystone.example.com/v3`
pub fn auth_url(mut self, auth_url: &str) -> Self {
if !auth_url.is_empty() {
self.config.auth_url = Some(auth_url.to_string());
}
self
}

/// Set the username for Keystone v3 authentication.
pub fn username(mut self, username: &str) -> Self {
if !username.is_empty() {
self.config.username = Some(username.to_string());
}
self
}

/// Set the password for Keystone v3 authentication.
pub fn password(mut self, password: &str) -> Self {
if !password.is_empty() {
self.config.password = Some(password.to_string());
}
self
}

/// Set the user domain name for Keystone v3 authentication.
///
/// Defaults to "Default" if not specified.
pub fn user_domain_name(mut self, name: &str) -> Self {
if !name.is_empty() {
self.config.user_domain_name = Some(name.to_string());
}
self
}

/// Set the project (tenant) name for Keystone v3 authentication.
pub fn project_name(mut self, name: &str) -> Self {
if !name.is_empty() {
self.config.project_name = Some(name.to_string());
}
self
}

/// Set the project domain name for Keystone v3 authentication.
///
/// Defaults to "Default" if not specified.
pub fn project_domain_name(mut self, name: &str) -> Self {
if !name.is_empty() {
self.config.project_domain_name = Some(name.to_string());
}
self
}
}

impl Builder for SwiftBuilder {
Expand All @@ -107,23 +165,6 @@ impl Builder for SwiftBuilder {
let root = normalize_root(&self.config.root.unwrap_or_default());
debug!("backend use root {root}");

let endpoint = match self.config.endpoint {
Some(endpoint) => {
if endpoint.starts_with("http") {
endpoint
} else {
format!("https://{endpoint}")
}
}
None => {
return Err(Error::new(
ErrorKind::ConfigInvalid,
"missing endpoint for Swift",
));
}
};
debug!("backend use endpoint: {}", &endpoint);

let container = match self.config.container {
Some(container) => container,
None => {
Expand All @@ -134,39 +175,139 @@ impl Builder for SwiftBuilder {
}
};

let token = self.config.token.unwrap_or_default();

Ok(SwiftBackend {
core: Arc::new(SwiftCore {
info: {
let am = AccessorInfo::default();
am.set_scheme(SWIFT_SCHEME)
.set_root(&root)
.set_native_capability(Capability {
stat: true,
read: true,

write: true,
write_can_empty: true,
write_with_user_metadata: true,

delete: true,

list: true,
list_with_recursive: true,

shared: true,

..Default::default()
});
am.into()
},
root,
endpoint,
container,
token,
}),
})
// Determine authentication mode and endpoint.
let has_keystone = self.config.auth_url.is_some();
let has_token = self.config.token.is_some();

let info: Arc<AccessorInfo> = {
let am = AccessorInfo::default();
am.set_scheme(SWIFT_SCHEME)
.set_root(&root)
.set_native_capability(Capability {
stat: true,
read: true,

write: true,
write_can_empty: true,
write_with_user_metadata: true,

delete: true,

list: true,
list_with_recursive: true,

shared: true,

..Default::default()
});
am.into()
};

if has_keystone {
// Keystone v3 authentication mode.
let auth_url = self.config.auth_url.unwrap();
let username = self.config.username.ok_or_else(|| {
Error::new(
ErrorKind::ConfigInvalid,
"username is required for Keystone v3 authentication",
)
})?;
let password = self.config.password.ok_or_else(|| {
Error::new(
ErrorKind::ConfigInvalid,
"password is required for Keystone v3 authentication",
)
})?;
let project_name = self.config.project_name.ok_or_else(|| {
Error::new(
ErrorKind::ConfigInvalid,
"project_name is required for Keystone v3 authentication",
)
})?;
let user_domain_name = self
.config
.user_domain_name
.unwrap_or_else(|| "Default".to_string());
let project_domain_name = self
.config
.project_domain_name
.unwrap_or_else(|| "Default".to_string());

let creds = KeystoneCredentials {
auth_url,
username,
password,
user_domain_name,
project_name,
project_domain_name,
};

let endpoint_lock = std::sync::OnceLock::new();

// If an explicit endpoint was provided, set it now.
if let Some(ep) = self.config.endpoint {
let ep = if ep.starts_with("http") {
ep
} else {
format!("https://{ep}")
};
let _ = endpoint_lock.set(ep);
}
// Otherwise it will be discovered from the Keystone catalog.

let signer = SwiftSigner::new_keystone(info.clone(), creds);

Ok(SwiftBackend {
core: Arc::new(SwiftCore {
info,
root,

endpoint: endpoint_lock,
container,
signer: Arc::new(Mutex::new(signer)),
}),
})
} else if has_token {
// Static token mode (existing behavior).
let endpoint = match self.config.endpoint {
Some(endpoint) => {
if endpoint.starts_with("http") {
endpoint
} else {
format!("https://{endpoint}")
}
}
None => {
return Err(Error::new(
ErrorKind::ConfigInvalid,
"missing endpoint for Swift",
));
}
};
debug!("backend use endpoint: {}", &endpoint);

let token = self.config.token.unwrap_or_default();
let signer = SwiftSigner::new_static(info.clone(), token);

let endpoint_lock = std::sync::OnceLock::new();
let _ = endpoint_lock.set(endpoint);

Ok(SwiftBackend {
core: Arc::new(SwiftCore {
info,
root,

endpoint: endpoint_lock,
container,
signer: Arc::new(Mutex::new(signer)),
}),
})
} else {
Err(Error::new(
ErrorKind::ConfigInvalid,
"either token or auth_url (with credentials) must be provided for Swift",
))
}
}
}

Expand All @@ -187,6 +328,9 @@ impl Access for SwiftBackend {
}

async fn stat(&self, path: &str, _args: OpStat) -> Result<RpStat> {
// Ensure endpoint is resolved (Keystone mode may need first auth).
self.core.ensure_endpoint().await?;

let resp = self.core.swift_get_metadata(path).await?;

match resp.status() {
Expand All @@ -205,6 +349,8 @@ impl Access for SwiftBackend {
}

async fn read(&self, path: &str, args: OpRead) -> Result<(RpRead, Self::Reader)> {
self.core.ensure_endpoint().await?;

let resp = self.core.swift_read(path, args.range(), &args).await?;

let status = resp.status();
Expand All @@ -220,6 +366,8 @@ impl Access for SwiftBackend {
}

async fn write(&self, path: &str, args: OpWrite) -> Result<(RpWrite, Self::Writer)> {
self.core.ensure_endpoint().await?;

let writer = SwiftWriter::new(self.core.clone(), args.clone(), path.to_string());

let w = oio::OneShotWriter::new(writer);
Expand All @@ -228,13 +376,17 @@ impl Access for SwiftBackend {
}

async fn delete(&self) -> Result<(RpDelete, Self::Deleter)> {
self.core.ensure_endpoint().await?;

Ok((
RpDelete::default(),
oio::OneShotDeleter::new(SwiftDeleter::new(self.core.clone())),
))
}

async fn list(&self, path: &str, args: OpList) -> Result<(RpList, Self::Lister)> {
self.core.ensure_endpoint().await?;

let l = SwiftLister::new(
self.core.clone(),
path.to_string(),
Expand All @@ -246,6 +398,8 @@ impl Access for SwiftBackend {
}

async fn copy(&self, from: &str, to: &str, _args: OpCopy) -> Result<RpCopy> {
self.core.ensure_endpoint().await?;

// cannot copy objects larger than 5 GB.
// Reference: https://docs.openstack.org/api-ref/object-store/#copy-object
let resp = self.core.swift_copy(from, to).await?;
Expand Down
28 changes: 26 additions & 2 deletions core/services/swift/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,37 @@ use super::backend::SwiftBuilder;
#[non_exhaustive]
pub struct SwiftConfig {
/// The endpoint for Swift.
///
/// When using Keystone v3 authentication, this can be omitted and will be
/// discovered from the service catalog.
pub endpoint: Option<String>,
/// The container for Swift.
pub container: Option<String>,
/// The root for Swift.
pub root: Option<String>,
/// The token for Swift.
///
/// When using Keystone v3 authentication, this is not needed — a token
/// will be acquired and refreshed automatically.
pub token: Option<String>,
/// The Keystone v3 authentication URL.
///
/// e.g. `https://keystone.example.com/v3`
pub auth_url: Option<String>,
/// The username for Keystone v3 authentication.
pub username: Option<String>,
/// The password for Keystone v3 authentication.
pub password: Option<String>,
/// The user domain name for Keystone v3 authentication.
///
/// Defaults to "Default" if not specified.
pub user_domain_name: Option<String>,
/// The project (tenant) name for Keystone v3 authentication.
pub project_name: Option<String>,
/// The project domain name for Keystone v3 authentication.
///
/// Defaults to "Default" if not specified.
pub project_domain_name: Option<String>,
}

impl Debug for SwiftConfig {
Expand All @@ -57,10 +81,10 @@ impl opendal_core::Configurator for SwiftConfig {
if let Some(authority) = uri.authority() {
map.entry("endpoint".to_string())
.or_insert_with(|| format!("https://{authority}"));
} else if !map.contains_key("endpoint") {
} else if !map.contains_key("endpoint") && !map.contains_key("auth_url") {
return Err(opendal_core::Error::new(
opendal_core::ErrorKind::ConfigInvalid,
"endpoint is required",
"endpoint or auth_url is required",
)
.with_context("service", SWIFT_SCHEME));
}
Expand Down
Loading