mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-30 01:58:59 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 381639fbbc | |||
| 1badff3923 | |||
| d5baaa67b8 | |||
| f5e715a8fd | |||
| e2f394a897 | |||
| 8a126bb176 | |||
| ae15f5804d |
@@ -190,7 +190,6 @@ jobs:
|
||||
{"target_id":"linux-x86_64-gnu","os":"sm-standard-2","target":"x86_64-unknown-linux-gnu","cross":false,"platform":"linux","rustflags":""},
|
||||
{"target_id":"linux-aarch64-gnu","os":"sm-standard-2","target":"aarch64-unknown-linux-gnu","cross":true,"platform":"linux","rustflags":""},
|
||||
{"target_id":"macos-aarch64","os":"macos-latest","target":"aarch64-apple-darwin","cross":false,"platform":"macos","rustflags":""},
|
||||
{"target_id":"macos-x86_64","os":"macos-26-intel","target":"x86_64-apple-darwin","cross":false,"platform":"macos","rustflags":""},
|
||||
{"target_id":"windows-x86_64","os":"windows-latest","target":"x86_64-pc-windows-msvc","cross":false,"platform":"windows","rustflags":""}
|
||||
]}'
|
||||
|
||||
@@ -352,10 +351,14 @@ jobs:
|
||||
rm -f console.zip console-release.json
|
||||
if [[ "$status" -eq 2 ]]; then
|
||||
echo "Console asset integrity verification failed" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Warning: Failed to download verified console assets, continuing without them"
|
||||
echo "// Static assets not available" > ./rustfs/static/empty.txt
|
||||
echo "Failed to download verified console assets" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -s ./rustfs/static/index.html ]]; then
|
||||
echo "Console asset archive is missing static/index.html" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Build RustFS
|
||||
@@ -559,6 +562,60 @@ jobs:
|
||||
echo "🔧 Build type: ${BUILD_TYPE}"
|
||||
echo "📊 Version: ${VERSION}"
|
||||
|
||||
- name: Verify packaged console
|
||||
if: matrix.target == 'x86_64-unknown-linux-gnu'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
find_free_port() {
|
||||
python3 - <<'PY'
|
||||
import socket
|
||||
|
||||
with socket.socket() as sock:
|
||||
sock.bind(("127.0.0.1", 0))
|
||||
print(sock.getsockname()[1])
|
||||
PY
|
||||
}
|
||||
|
||||
package_dir="$(mktemp -d)"
|
||||
data_dir="$(mktemp -d)"
|
||||
log_file="${RUNNER_TEMP}/rustfs-console-smoke.log"
|
||||
server_pid=""
|
||||
trap '
|
||||
if [[ -n "${server_pid:-}" ]]; then
|
||||
kill "$server_pid" 2>/dev/null || true
|
||||
wait "$server_pid" 2>/dev/null || true
|
||||
fi
|
||||
rm -rf "$package_dir" "$data_dir"
|
||||
' EXIT
|
||||
|
||||
unzip -q "${{ steps.package.outputs.package_file }}" -d "$package_dir"
|
||||
api_port="$(find_free_port)"
|
||||
console_port="$(find_free_port)"
|
||||
console_url="http://127.0.0.1:${console_port}/rustfs/console/"
|
||||
|
||||
"$package_dir/rustfs" server \
|
||||
--address "127.0.0.1:${api_port}" \
|
||||
--console-enable \
|
||||
--console-address "127.0.0.1:${console_port}" \
|
||||
--access-key console-smoke \
|
||||
--secret-key console-smoke-secret \
|
||||
"$data_dir" >"$log_file" 2>&1 &
|
||||
server_pid=$!
|
||||
|
||||
for _ in {1..40}; do
|
||||
response="$(curl --silent --output /dev/null --write-out '%{http_code} %{content_type}' "$console_url" || true)"
|
||||
if [[ "$response" == 200\ text/html* ]]; then
|
||||
exit 0
|
||||
fi
|
||||
sleep 0.25
|
||||
done
|
||||
|
||||
echo "Console endpoint did not return 200 text/html: $response" >&2
|
||||
cat "$log_file"
|
||||
exit 1
|
||||
|
||||
- name: Upload to GitHub artifacts
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
with:
|
||||
@@ -849,12 +906,14 @@ jobs:
|
||||
|
||||
echo "✅ All assets uploaded successfully"
|
||||
|
||||
# Update latest.json for stable releases only: prerelease tags (alpha/beta/
|
||||
# rc) must never overwrite the stable version pointer.
|
||||
# Update latest.json for every release tag (stable and prerelease): the
|
||||
# project currently ships prerelease tags only, so gating this to stable
|
||||
# left the version pointer permanently stale. release_type records whether
|
||||
# the pointed-to version is a prerelease.
|
||||
update-latest-version:
|
||||
name: Update Latest Version
|
||||
needs: [ build-check, upload-release-assets ]
|
||||
if: startsWith(github.ref, 'refs/tags/') && needs.build-check.outputs.build_type == 'release'
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Update latest.json
|
||||
@@ -873,6 +932,12 @@ jobs:
|
||||
VERSION="${{ needs.build-check.outputs.version }}"
|
||||
TAG="${{ needs.build-check.outputs.version }}"
|
||||
|
||||
if [[ "${{ needs.build-check.outputs.build_type }}" == "prerelease" ]]; then
|
||||
RELEASE_TYPE="prerelease"
|
||||
else
|
||||
RELEASE_TYPE="stable"
|
||||
fi
|
||||
|
||||
# Install ossutil
|
||||
OSSUTIL_VERSION="2.1.1"
|
||||
OSSUTIL_ZIP="ossutil-${OSSUTIL_VERSION}-linux-amd64.zip"
|
||||
@@ -893,7 +958,7 @@ jobs:
|
||||
"version": "${VERSION}",
|
||||
"tag": "${TAG}",
|
||||
"release_date": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
||||
"release_type": "stable",
|
||||
"release_type": "${RELEASE_TYPE}",
|
||||
"download_url": "https://github.com/${{ github.repository }}/releases/tag/${TAG}"
|
||||
}
|
||||
EOF
|
||||
@@ -901,7 +966,7 @@ jobs:
|
||||
# Upload to OSS
|
||||
"$OSSUTIL_BIN" cp latest.json oss://rustfs-version/latest.json --force
|
||||
|
||||
echo "✅ Updated latest.json for stable release $VERSION"
|
||||
echo "✅ Updated latest.json for ${RELEASE_TYPE} release $VERSION"
|
||||
|
||||
# Publish release (remove draft status)
|
||||
publish-release:
|
||||
|
||||
@@ -221,15 +221,29 @@ jobs:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
cache-save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
# Three scanner tests fail on main independently of this lane (restore of
|
||||
# a transitioned multipart object, noncurrent transition/expiry after an
|
||||
# immediate compensation transition); they keep #[ignore] with a backlog
|
||||
# reference and are excluded here by name until fixed (rustfs/backlog#1148).
|
||||
# The #4877 restore self-deadlock is fixed in this PR, which re-enabled
|
||||
# test_multipart_restore_preserves_parts_and_etag. The remaining exclusions
|
||||
# each hit a DIFFERENT, independent issue (all tracked under
|
||||
# rustfs/backlog#1148; they keep #[ignore] with a backlog reference):
|
||||
# - test_noncurrent_{expiry,transition}_still_works_after_immediate_compensation_transition:
|
||||
# noncurrent transition/expiry after an immediate compensation transition.
|
||||
# - test_transition_and_restore_flows: transition metadata is missing on
|
||||
# one drive (assert_transition_meta_consistent: "missing xl.meta ... on
|
||||
# disk2") - an EC metadata-distribution issue, not the restore lock.
|
||||
# - test_restore_chain_local_read_expiry_keeps_remote_and_allows_re_restore:
|
||||
# DeleteRestoredAction sets opts.transition.expire_restored, but no
|
||||
# delete path reads that flag, so cleanup deletes the whole object
|
||||
# instead of only the local restored copy (ObjectNotFound afterwards).
|
||||
# The expire_restored delete semantics are unimplemented.
|
||||
# - restore_object_usecase_reports_ongoing_conflict_and_completion: asserts
|
||||
# a concurrent get_object_info observes ongoing-request=true mid-restore,
|
||||
# which #4877's read-vs-restore serialization rules out (see backlog#1148
|
||||
# ilm-8 criterion 1 - an API-semantics decision, not a bug).
|
||||
- name: Run ignored ILM integration tests serially
|
||||
run: |
|
||||
cargo nextest run -j1 --run-ignored ignored-only \
|
||||
-p rustfs-scanner -p rustfs \
|
||||
-E '(binary(lifecycle_integration_test) or (package(rustfs) and test(lifecycle_transition_api_test))) and not (test(test_transition_and_restore_flows) or test(test_noncurrent_expiry_still_works_after_immediate_compensation_transition) or test(test_noncurrent_transition_still_works_after_immediate_compensation_transition))'
|
||||
-E '(binary(lifecycle_integration_test) or (package(rustfs) and test(lifecycle_transition_api_test))) and not (test(test_transition_and_restore_flows) or test(test_noncurrent_expiry_still_works_after_immediate_compensation_transition) or test(test_noncurrent_transition_still_works_after_immediate_compensation_transition) or test(restore_object_usecase_reports_ongoing_conflict_and_completion) or test(test_restore_chain_local_read_expiry_keeps_remote_and_allows_re_restore))'
|
||||
|
||||
test-and-lint-rio-v2:
|
||||
name: Test and Lint (rio-v2)
|
||||
|
||||
@@ -26,10 +26,7 @@ use crate::disk::{
|
||||
error::{DiskError, Error, FileAccessDeniedWithContext, Result},
|
||||
error_conv::{to_access_error, to_file_error, to_unformatted_disk_error, to_volume_error},
|
||||
format::FormatV3,
|
||||
fs::{
|
||||
O_APPEND, O_CREATE, O_RDONLY, O_TRUNC, O_WRONLY, access, access_std, lstat, lstat_std, remove, remove_all_std,
|
||||
remove_std, rename,
|
||||
},
|
||||
fs::{O_APPEND, O_CREATE, O_RDONLY, O_TRUNC, O_WRONLY, access, lstat, lstat_std, remove, remove_all_std, remove_std, rename},
|
||||
os,
|
||||
os::{check_path_length, is_empty_dir, is_root_disk, rename_all, rename_all_ignore_missing_source},
|
||||
};
|
||||
@@ -101,7 +98,7 @@ fn inline_metadata_rollback_dir(version_id: Uuid, meta: &FileMeta) -> Uuid {
|
||||
fn remove_file_if_exists(path: &Path) -> std::io::Result<()> {
|
||||
match std::fs::remove_file(path) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
|
||||
Err(err) if err.kind() == ErrorKind::NotFound => Ok(()),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
@@ -109,7 +106,7 @@ fn remove_file_if_exists(path: &Path) -> std::io::Result<()> {
|
||||
fn remove_dir_all_if_exists(path: &Path) -> std::io::Result<()> {
|
||||
match std::fs::remove_dir_all(path) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
|
||||
Err(err) if err.kind() == ErrorKind::NotFound => Ok(()),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
@@ -121,7 +118,7 @@ fn rollback_committed_rename_std(
|
||||
) -> std::io::Result<()> {
|
||||
if let Some(old_data_dir) = rollback_data_dir {
|
||||
let Some(dst_parent) = dst_file_path.parent() else {
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, "missing object metadata parent"));
|
||||
return Err(std::io::Error::new(ErrorKind::InvalidInput, "missing object metadata parent"));
|
||||
};
|
||||
let backup_path = dst_parent.join(old_data_dir.to_string()).join(STORAGE_FORMAT_FILE_BACKUP);
|
||||
std::fs::rename(backup_path, dst_file_path)?;
|
||||
@@ -591,7 +588,7 @@ impl DurabilityMode {
|
||||
"strict" => Some(Self::Strict),
|
||||
"relaxed" => Some(Self::Relaxed),
|
||||
"none" => Some(Self::None),
|
||||
_ => Option::None,
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -799,7 +796,7 @@ pub(crate) mod bucket_durability {
|
||||
/// configured durability mode: their contents commit into user buckets and
|
||||
/// are exactly the writes the relaxed tiers exist for.
|
||||
fn is_scratch_volume(volume: &str) -> bool {
|
||||
for scratch in [super::RUSTFS_META_TMP_BUCKET, super::RUSTFS_META_MULTIPART_BUCKET] {
|
||||
for scratch in [RUSTFS_META_TMP_BUCKET, super::RUSTFS_META_MULTIPART_BUCKET] {
|
||||
if volume == scratch || volume.strip_prefix(scratch).is_some_and(|rest| rest.starts_with('/')) {
|
||||
return true;
|
||||
}
|
||||
@@ -816,7 +813,7 @@ fn is_system_critical_volume(volume: &str) -> bool {
|
||||
if is_scratch_volume(volume) {
|
||||
return false;
|
||||
}
|
||||
for meta in [super::RUSTFS_META_BUCKET, super::MIGRATING_META_BUCKET] {
|
||||
for meta in [RUSTFS_META_BUCKET, super::MIGRATING_META_BUCKET] {
|
||||
if volume == meta || volume.strip_prefix(meta).is_some_and(|rest| rest.starts_with('/')) {
|
||||
return true;
|
||||
}
|
||||
@@ -935,12 +932,12 @@ struct AlignedBuf {
|
||||
impl AlignedBuf {
|
||||
fn new(len: usize, align: usize) -> std::io::Result<Self> {
|
||||
debug_assert!(len > 0, "AlignedBuf must not be zero-sized");
|
||||
let layout = std::alloc::Layout::from_size_align(len, align)
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
|
||||
let layout =
|
||||
std::alloc::Layout::from_size_align(len, align).map_err(|e| std::io::Error::new(ErrorKind::InvalidInput, e))?;
|
||||
// SAFETY: `layout` has non-zero size (callers guarantee len > 0) and a
|
||||
// valid power-of-two alignment enforced by Layout::from_size_align.
|
||||
let ptr = unsafe { std::alloc::alloc_zeroed(layout) };
|
||||
let ptr = std::ptr::NonNull::new(ptr).ok_or(std::io::ErrorKind::OutOfMemory)?;
|
||||
let ptr = std::ptr::NonNull::new(ptr).ok_or(ErrorKind::OutOfMemory)?;
|
||||
Ok(Self { ptr, len, layout })
|
||||
}
|
||||
|
||||
@@ -994,10 +991,9 @@ fn pread_direct_aligned(file_path: &Path, offset: u64, length: usize, state: &Di
|
||||
let align_u64 = align as u64;
|
||||
|
||||
let aligned_offset = offset - (offset % align_u64);
|
||||
let logical_start =
|
||||
usize::try_from(offset - aligned_offset).map_err(|_| std::io::Error::from(std::io::ErrorKind::InvalidInput))?;
|
||||
let logical_end = logical_start.checked_add(length).ok_or(std::io::ErrorKind::InvalidInput)?;
|
||||
let aligned_len = logical_end.checked_add(align - 1).ok_or(std::io::ErrorKind::InvalidInput)? / align * align;
|
||||
let logical_start = usize::try_from(offset - aligned_offset).map_err(|_| std::io::Error::from(ErrorKind::InvalidInput))?;
|
||||
let logical_end = logical_start.checked_add(length).ok_or(ErrorKind::InvalidInput)?;
|
||||
let aligned_len = logical_end.checked_add(align - 1).ok_or(ErrorKind::InvalidInput)? / align * align;
|
||||
|
||||
let mut buf = AlignedBuf::new(aligned_len, align)?;
|
||||
|
||||
@@ -1012,7 +1008,7 @@ fn pread_direct_aligned(file_path: &Path, offset: u64, length: usize, state: &Di
|
||||
filled += n;
|
||||
}
|
||||
if filled < logical_end {
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "short O_DIRECT read"));
|
||||
return Err(std::io::Error::new(ErrorKind::UnexpectedEof, "short O_DIRECT read"));
|
||||
}
|
||||
|
||||
Ok(Bytes::copy_from_slice(&buf.as_slice()[logical_start..logical_end]))
|
||||
@@ -1094,10 +1090,7 @@ fn pwrite_all(file: &std::fs::File, mut buf: &[u8], mut offset: u64) -> std::io:
|
||||
while !buf.is_empty() {
|
||||
let n = file.write_at(buf, offset)?;
|
||||
if n == 0 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::WriteZero,
|
||||
"O_DIRECT positioned write wrote 0 bytes",
|
||||
));
|
||||
return Err(std::io::Error::new(ErrorKind::WriteZero, "O_DIRECT positioned write wrote 0 bytes"));
|
||||
}
|
||||
buf = &buf[n..];
|
||||
offset += n as u64;
|
||||
@@ -2128,7 +2121,8 @@ impl LocalIoBackend for StdBackend {
|
||||
let access_check_start = metrics_enabled.then(StdInstant::now);
|
||||
let volume_dir = local_disk_bucket_path(&root, &volume_owned)?;
|
||||
if !skip_access_checks(&volume_owned) {
|
||||
access_std(&volume_dir).map_err(|e| DiskError::from(to_access_error(e, DiskError::VolumeAccessDenied)))?;
|
||||
crate::disk::fs::access_std(&volume_dir)
|
||||
.map_err(|e| DiskError::from(to_access_error(e, DiskError::VolumeAccessDenied)))?;
|
||||
}
|
||||
let access_check_duration = access_check_start.map_or(StdDuration::ZERO, |started_at| started_at.elapsed());
|
||||
|
||||
@@ -2948,7 +2942,8 @@ fn is_io_uring_unsupported(err: &std::io::Error) -> bool {
|
||||
fn resolve_uring_object_path(root: &Path, volume: &str, path: &str) -> Result<PathBuf> {
|
||||
let volume_dir = local_disk_bucket_path(root, volume)?;
|
||||
if !skip_access_checks(volume) {
|
||||
access_std(&volume_dir).map_err(|e| DiskError::from(to_access_error(e, DiskError::VolumeAccessDenied)))?;
|
||||
crate::disk::fs::access_std(&volume_dir)
|
||||
.map_err(|e| DiskError::from(to_access_error(e, DiskError::VolumeAccessDenied)))?;
|
||||
}
|
||||
let file_path = local_disk_object_path(root, volume, path)?;
|
||||
check_path_length(file_path.to_string_lossy().as_ref())?;
|
||||
@@ -3797,10 +3792,9 @@ impl LocalDisk {
|
||||
// rebuilding (see rustfs-heal); surface it so scanner
|
||||
// coordination, lock selection and admin/metrics see
|
||||
// the rebuild. Refreshed with this cache (~1s).
|
||||
let healing =
|
||||
tokio::fs::try_exists(root.join(super::RUSTFS_META_BUCKET).join(super::HEALING_MARKER_PATH))
|
||||
.await
|
||||
.unwrap_or(false);
|
||||
let healing = tokio::fs::try_exists(root.join(RUSTFS_META_BUCKET).join(super::HEALING_MARKER_PATH))
|
||||
.await
|
||||
.unwrap_or(false);
|
||||
let disk_info = DiskInfo {
|
||||
total: info.total,
|
||||
free: info.free,
|
||||
@@ -4780,8 +4774,8 @@ impl LocalDisk {
|
||||
let file_path = self.get_object_path(volume, path)?;
|
||||
check_path_length(file_path.to_string_lossy().as_ref())?;
|
||||
|
||||
let tmp_volume_dir = self.get_bucket_path(super::RUSTFS_META_TMP_BUCKET)?;
|
||||
let tmp_file_path = self.get_object_path(super::RUSTFS_META_TMP_BUCKET, Uuid::new_v4().to_string().as_str())?;
|
||||
let tmp_volume_dir = self.get_bucket_path(RUSTFS_META_TMP_BUCKET)?;
|
||||
let tmp_file_path = self.get_object_path(RUSTFS_META_TMP_BUCKET, Uuid::new_v4().to_string().as_str())?;
|
||||
|
||||
let durability = effective_durability(volume);
|
||||
|
||||
@@ -5641,7 +5635,7 @@ async fn read_file_metadata(p: impl AsRef<Path>) -> Result<Metadata> {
|
||||
fn skip_access_checks(p: impl AsRef<str>) -> bool {
|
||||
let vols = [
|
||||
RUSTFS_META_TMP_DELETED_BUCKET,
|
||||
super::RUSTFS_META_TMP_BUCKET,
|
||||
RUSTFS_META_TMP_BUCKET,
|
||||
super::RUSTFS_META_MULTIPART_BUCKET,
|
||||
RUSTFS_META_BUCKET,
|
||||
];
|
||||
@@ -5952,12 +5946,11 @@ impl DiskAPI for LocalDisk {
|
||||
);
|
||||
for (i, part) in fi.parts.iter().enumerate() {
|
||||
let checksum_info = erasure.get_checksum_info(part.number);
|
||||
let checksum_algo =
|
||||
if fi.uses_legacy_checksum && checksum_info.algorithm == rustfs_utils::HashAlgorithm::HighwayHash256S {
|
||||
rustfs_utils::HashAlgorithm::HighwayHash256SLegacy
|
||||
} else {
|
||||
checksum_info.algorithm
|
||||
};
|
||||
let checksum_algo = if fi.uses_legacy_checksum && checksum_info.algorithm == HashAlgorithm::HighwayHash256S {
|
||||
HashAlgorithm::HighwayHash256SLegacy
|
||||
} else {
|
||||
checksum_info.algorithm
|
||||
};
|
||||
let part_path = self.get_object_path(
|
||||
volume,
|
||||
path_join_buf(&[
|
||||
@@ -6985,7 +6978,7 @@ impl DiskAPI for LocalDisk {
|
||||
// Read existing xl.meta
|
||||
let has_dst_buf = match std::fs::read(&dst) {
|
||||
Ok(buf) => Some(Bytes::from(buf)),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
|
||||
Err(e) if e.kind() == ErrorKind::NotFound => None,
|
||||
Err(e) => return Err(to_file_error(e)),
|
||||
};
|
||||
|
||||
@@ -7077,8 +7070,8 @@ impl DiskAPI for LocalDisk {
|
||||
|
||||
match std::fs::rename(&src, &dst) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound && !src.exists() => Ok(()),
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||
Err(err) if err.kind() == ErrorKind::NotFound && !src.exists() => Ok(()),
|
||||
Err(err) if err.kind() == ErrorKind::NotFound => {
|
||||
if let Some(parent) = dst.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
@@ -7126,7 +7119,7 @@ impl DiskAPI for LocalDisk {
|
||||
}
|
||||
}
|
||||
|
||||
Ok::<(Option<uuid::Uuid>, Option<Vec<u8>>, Option<OldCurrentSize>), std::io::Error>((
|
||||
Ok::<(Option<Uuid>, Option<Vec<u8>>, Option<OldCurrentSize>), std::io::Error>((
|
||||
rollback_data_dir,
|
||||
version_signature,
|
||||
old_current_size,
|
||||
@@ -8110,7 +8103,7 @@ mod test {
|
||||
|
||||
/// Crash-consistency harness for the rename_data commit sequence
|
||||
/// (rustfs/backlog#935 HP-14, test plan rustfs/backlog#896; hard rule from
|
||||
/// rustfs/backlog#878: "partial commit 后对象只能是旧版本或新版本,不能混合").
|
||||
/// rustfs/backlog#878: "After partial commit, the object can only be an old or new version; it cannot be mixed").
|
||||
///
|
||||
/// For every pre-commit crash point × durability tier, it seeds a committed
|
||||
/// object, stages a replacement, injects a hard power loss (no in-process
|
||||
@@ -8449,7 +8442,7 @@ mod test {
|
||||
/// Backdate a path's mtime so zero-expiry cleanup tests classify it as
|
||||
/// stale deterministically, instead of sleeping and hoping the filesystem
|
||||
/// timestamp granularity (or a backward wall-clock step) cooperates.
|
||||
fn backdate_mtime(path: &std::path::Path, age: Duration) {
|
||||
fn backdate_mtime(path: &Path, age: Duration) {
|
||||
use std::fs::{File, FileTimes};
|
||||
let mtime = std::time::SystemTime::now() - age;
|
||||
File::open(path)
|
||||
@@ -8858,17 +8851,13 @@ mod test {
|
||||
async fn blocking_scan_writer_keeps_flush_and_shutdown_pending() {
|
||||
let mut flush_writer = BlockingScanWriter { entered_tx: None };
|
||||
assert!(
|
||||
tokio::time::timeout(Duration::from_millis(10), flush_writer.flush())
|
||||
.await
|
||||
.is_err(),
|
||||
timeout(Duration::from_millis(10), flush_writer.flush()).await.is_err(),
|
||||
"blocking scan writer flush should stay pending"
|
||||
);
|
||||
|
||||
let mut shutdown_writer = BlockingScanWriter { entered_tx: None };
|
||||
assert!(
|
||||
tokio::time::timeout(Duration::from_millis(10), shutdown_writer.shutdown())
|
||||
.await
|
||||
.is_err(),
|
||||
timeout(Duration::from_millis(10), shutdown_writer.shutdown()).await.is_err(),
|
||||
"blocking scan writer shutdown should stay pending"
|
||||
);
|
||||
}
|
||||
@@ -8877,15 +8866,11 @@ mod test {
|
||||
/// consumer (quorum merge, a lagging peer drive).
|
||||
struct SlowWriter {
|
||||
delay: Duration,
|
||||
sleep: Option<std::pin::Pin<Box<Sleep>>>,
|
||||
sleep: Option<Pin<Box<Sleep>>>,
|
||||
}
|
||||
|
||||
impl AsyncWrite for SlowWriter {
|
||||
fn poll_write(
|
||||
mut self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> std::task::Poll<std::io::Result<usize>> {
|
||||
fn poll_write(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll<io::Result<usize>> {
|
||||
if self.sleep.is_none() {
|
||||
let delay = self.delay;
|
||||
self.sleep = Some(Box::pin(tokio::time::sleep(delay)));
|
||||
@@ -8893,23 +8878,20 @@ mod test {
|
||||
|
||||
let sleep = self.sleep.as_mut().expect("sleep was just installed");
|
||||
match sleep.as_mut().poll(cx) {
|
||||
std::task::Poll::Ready(()) => {
|
||||
Poll::Ready(()) => {
|
||||
self.sleep = None;
|
||||
std::task::Poll::Ready(Ok(buf.len()))
|
||||
Poll::Ready(Ok(buf.len()))
|
||||
}
|
||||
std::task::Poll::Pending => std::task::Poll::Pending,
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_flush(self: std::pin::Pin<&mut Self>, _cx: &mut std::task::Context<'_>) -> std::task::Poll<std::io::Result<()>> {
|
||||
std::task::Poll::Ready(Ok(()))
|
||||
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
|
||||
fn poll_shutdown(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
_cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<std::io::Result<()>> {
|
||||
std::task::Poll::Ready(Ok(()))
|
||||
fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9605,14 +9587,14 @@ mod test {
|
||||
bucket_durability::set(super::super::RUSTFS_META_MULTIPART_BUCKET, Some(DurabilityMode::Relaxed));
|
||||
bucket_durability::set("", Some(DurabilityMode::Relaxed));
|
||||
|
||||
assert_eq!(bucket_durability::lookup(RUSTFS_META_BUCKET), Option::None);
|
||||
assert_eq!(bucket_durability::lookup(RUSTFS_META_TMP_BUCKET), Option::None);
|
||||
assert_eq!(bucket_durability::lookup(RUSTFS_META_BUCKET), None);
|
||||
assert_eq!(bucket_durability::lookup(RUSTFS_META_TMP_BUCKET), None);
|
||||
assert_eq!(effective_durability(RUSTFS_META_BUCKET), DurabilityMode::Strict);
|
||||
|
||||
// The legacy full-off tier is process-wide only: registering it per
|
||||
// bucket is dropped, not stored.
|
||||
bucket_durability::set("hp5b-legacy-refused", Some(DurabilityMode::LegacyOff));
|
||||
assert_eq!(bucket_durability::lookup("hp5b-legacy-refused"), Option::None);
|
||||
assert_eq!(bucket_durability::lookup("hp5b-legacy-refused"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -11071,7 +11053,7 @@ mod test {
|
||||
|
||||
let vols = [
|
||||
RUSTFS_META_TMP_DELETED_BUCKET,
|
||||
super::super::RUSTFS_META_TMP_BUCKET,
|
||||
RUSTFS_META_TMP_BUCKET,
|
||||
super::super::RUSTFS_META_MULTIPART_BUCKET,
|
||||
RUSTFS_META_BUCKET,
|
||||
];
|
||||
@@ -11110,7 +11092,7 @@ mod test {
|
||||
let mut reader = StallTimeoutReader::new(PendingTestReader, Duration::ZERO);
|
||||
let mut buf = [0; 1];
|
||||
|
||||
let result = tokio::time::timeout(Duration::from_millis(10), reader.read(&mut buf)).await;
|
||||
let result = timeout(Duration::from_millis(10), reader.read(&mut buf)).await;
|
||||
|
||||
assert!(result.is_err(), "zero timeout must leave stalled reads pending instead of failing");
|
||||
}
|
||||
@@ -11240,10 +11222,10 @@ mod test {
|
||||
|
||||
#[tokio::test(start_paused = true)]
|
||||
async fn cleanup_loop_interval_does_not_tick_immediately() {
|
||||
let start_at = tokio::time::Instant::now() + DELETED_OBJECTS_CLEANUP_INTERVAL;
|
||||
let start_at = Instant::now() + DELETED_OBJECTS_CLEANUP_INTERVAL;
|
||||
let mut interval = interval_at(start_at, DELETED_OBJECTS_CLEANUP_INTERVAL);
|
||||
|
||||
assert!(tokio::time::timeout(Duration::from_secs(1), interval.tick()).await.is_err());
|
||||
assert!(timeout(Duration::from_secs(1), interval.tick()).await.is_err());
|
||||
|
||||
tokio::time::advance(DELETED_OBJECTS_CLEANUP_INTERVAL).await;
|
||||
interval.tick().await;
|
||||
@@ -11291,26 +11273,16 @@ mod test {
|
||||
struct BrokenPipeWriter;
|
||||
|
||||
impl AsyncWrite for BrokenPipeWriter {
|
||||
fn poll_write(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
_cx: &mut std::task::Context<'_>,
|
||||
_buf: &[u8],
|
||||
) -> std::task::Poll<std::io::Result<usize>> {
|
||||
std::task::Poll::Ready(Err(std::io::Error::new(std::io::ErrorKind::BrokenPipe, "closed")))
|
||||
fn poll_write(self: Pin<&mut Self>, _cx: &mut Context<'_>, _buf: &[u8]) -> Poll<io::Result<usize>> {
|
||||
Poll::Ready(Err(io::Error::new(ErrorKind::BrokenPipe, "closed")))
|
||||
}
|
||||
|
||||
fn poll_flush(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
_cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<std::io::Result<()>> {
|
||||
std::task::Poll::Ready(Ok(()))
|
||||
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
|
||||
fn poll_shutdown(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
_cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<std::io::Result<()>> {
|
||||
std::task::Poll::Ready(Ok(()))
|
||||
fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12442,7 +12414,7 @@ mod test {
|
||||
fs::set_permissions(&meta_path, Permissions::from_mode(0o000))
|
||||
.await
|
||||
.expect("operation should succeed");
|
||||
if fs::File::open(&meta_path).await.is_ok() {
|
||||
if File::open(&meta_path).await.is_ok() {
|
||||
fs::set_permissions(&meta_path, original_permissions)
|
||||
.await
|
||||
.expect("operation should succeed");
|
||||
@@ -12977,7 +12949,7 @@ mod test {
|
||||
// It only checks length and platform-specific special characters
|
||||
// System volume names are valid according to the current implementation
|
||||
assert!(LocalDisk::is_valid_volname(RUSTFS_META_BUCKET));
|
||||
assert!(LocalDisk::is_valid_volname(super::super::RUSTFS_META_TMP_BUCKET));
|
||||
assert!(LocalDisk::is_valid_volname(RUSTFS_META_TMP_BUCKET));
|
||||
|
||||
// Testing platform-specific behavior for special characters
|
||||
#[cfg(windows)]
|
||||
@@ -13321,24 +13293,20 @@ mod test {
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
for (kind, expected) in [("minor", 1), ("major", 2)] {
|
||||
for (kind, expected) in [("minor", 1), ("major", 2)].iter().copied() {
|
||||
let labels = [("path", "local_test"), ("stage", "mmap_map"), ("kind", kind)];
|
||||
assert_eq!(
|
||||
recorder.counter_value(
|
||||
METRIC_GET_OBJECT_MMAP_PAGE_FAULTS_TOTAL,
|
||||
&[("path", "local_test"), ("stage", "mmap_map"), ("kind", kind)]
|
||||
),
|
||||
recorder.counter_value(METRIC_GET_OBJECT_MMAP_PAGE_FAULTS_TOTAL, &labels),
|
||||
expected,
|
||||
"zero deltas must not emit and positive {kind} deltas must accumulate exactly"
|
||||
"zero deltas must not emit and positive mmap page fault deltas must accumulate exactly"
|
||||
);
|
||||
}
|
||||
for (kind, expected) in [("minor", 3), ("major", 4)] {
|
||||
for (kind, expected) in [("minor", 3), ("major", 4)].iter().copied() {
|
||||
let labels = [("path", "local_test"), ("stage", "direct_read_copy"), ("kind", kind)];
|
||||
assert_eq!(
|
||||
recorder.counter_value(
|
||||
METRIC_GET_OBJECT_DIRECT_READ_PAGE_FAULTS_TOTAL,
|
||||
&[("path", "local_test"), ("stage", "direct_read_copy"), ("kind", kind)]
|
||||
),
|
||||
recorder.counter_value(METRIC_GET_OBJECT_DIRECT_READ_PAGE_FAULTS_TOTAL, &labels),
|
||||
expected,
|
||||
"zero deltas must not emit and positive {kind} deltas must accumulate exactly"
|
||||
"zero deltas must not emit and positive direct-read page fault deltas must accumulate exactly"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -13553,15 +13521,15 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn test_is_bitrot_size_mismatch_error_only_matches_target_message() {
|
||||
assert!(is_bitrot_size_mismatch_error(&std::io::Error::other("bitrot shard file size mismatch")));
|
||||
assert!(!is_bitrot_size_mismatch_error(&std::io::Error::other("bitrot hash mismatch")));
|
||||
assert!(is_bitrot_size_mismatch_error(&io::Error::other("bitrot shard file size mismatch")));
|
||||
assert!(!is_bitrot_size_mismatch_error(&io::Error::other("bitrot hash mismatch")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_bitrot_verification_error_matches_hash_and_size_mismatch() {
|
||||
assert!(is_bitrot_verification_error(&std::io::Error::other("bitrot shard file size mismatch")));
|
||||
assert!(is_bitrot_verification_error(&std::io::Error::other("bitrot hash mismatch")));
|
||||
assert!(!is_bitrot_verification_error(&std::io::Error::other("unrelated io failure")));
|
||||
assert!(is_bitrot_verification_error(&io::Error::other("bitrot shard file size mismatch")));
|
||||
assert!(is_bitrot_verification_error(&io::Error::other("bitrot hash mismatch")));
|
||||
assert!(!is_bitrot_verification_error(&io::Error::other("unrelated io failure")));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -13600,7 +13568,7 @@ mod test {
|
||||
..Default::default()
|
||||
}];
|
||||
|
||||
let mut writer = BitrotWriter::new(std::io::Cursor::new(Vec::new()), file_info.erasure.shard_size(), checksum_algo);
|
||||
let mut writer = BitrotWriter::new(io::Cursor::new(Vec::new()), file_info.erasure.shard_size(), checksum_algo);
|
||||
writer
|
||||
.write(&payload)
|
||||
.await
|
||||
@@ -13667,7 +13635,7 @@ mod test {
|
||||
} else {
|
||||
HashAlgorithm::HighwayHash256S
|
||||
};
|
||||
let mut writer = BitrotWriter::new(std::io::Cursor::new(Vec::new()), codec_erasure.shard_size(), checksum_algo);
|
||||
let mut writer = BitrotWriter::new(io::Cursor::new(Vec::new()), codec_erasure.shard_size(), checksum_algo);
|
||||
writer.write(&payload[..8]).await.expect("first shard block should encode");
|
||||
writer.write(&payload[8..]).await.expect("final shard block should encode");
|
||||
writer.shutdown().await.expect("bitrot writer should flush test payload");
|
||||
@@ -14524,7 +14492,7 @@ mod test {
|
||||
// SAFETY: `map.as_ptr()` is page-aligned and `len` bytes long; `vec` has
|
||||
// one byte per page of that range, which is what mincore writes.
|
||||
let rc = unsafe { libc::mincore(map.as_ptr() as *mut libc::c_void, len, vec.as_mut_ptr()) };
|
||||
assert_eq!(rc, 0, "mincore failed: {}", std::io::Error::last_os_error());
|
||||
assert_eq!(rc, 0, "mincore failed: {}", io::Error::last_os_error());
|
||||
vec.iter().filter(|b| *b & 1 == 1).count()
|
||||
}
|
||||
|
||||
|
||||
@@ -2384,7 +2384,16 @@ impl crate::storage_api_contracts::object::ObjectOperations for SetDisks {
|
||||
let (actual_fi, _, _) = fi?;
|
||||
|
||||
oi = ObjectInfo::from_file_info(&actual_fi, bucket, object, opts.versioned || opts.version_suspended);
|
||||
let ropts = put_restore_opts(bucket, object, &opts.transition.restore_request, &oi).await?;
|
||||
let mut ropts = put_restore_opts(bucket, object, &opts.transition.restore_request, &oi).await?;
|
||||
// The restore copy-back re-writes this same object via put_object /
|
||||
// new_multipart_upload / complete_multipart_upload, each of which takes
|
||||
// the object write lock in its commit phase. The caller
|
||||
// (handle_restore_transitioned_object, #4877) already holds that write
|
||||
// lock for the whole restore and forwards no_lock=true, so the inner
|
||||
// writes must inherit it or they self-deadlock on the lock we already
|
||||
// hold and time out. put_restore_opts builds fresh options that default
|
||||
// no_lock=false, so propagate it explicitly here.
|
||||
ropts.no_lock = opts.no_lock;
|
||||
if oi.parts.len() == 1 {
|
||||
let mut opts = opts.clone();
|
||||
opts.part_number = Some(1);
|
||||
@@ -2502,6 +2511,9 @@ impl crate::storage_api_contracts::object::ObjectOperations for SetDisks {
|
||||
uploaded_parts,
|
||||
&ObjectOptions {
|
||||
mod_time: oi.mod_time,
|
||||
// Inherit the restore write lock (see ropts.no_lock above):
|
||||
// the commit phase re-acquires this object's write lock.
|
||||
no_lock: opts.no_lock,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
|
||||
@@ -157,8 +157,8 @@ impl LocalKmsClient {
|
||||
Ok(salt)
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
async fn set_file_permissions(path: &std::path::Path, permissions: Option<u32>) -> Result<()> {
|
||||
#[cfg(unix)]
|
||||
if let Some(mode) = permissions {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
@@ -166,7 +166,11 @@ impl LocalKmsClient {
|
||||
fs::set_permissions(path, perms).await?;
|
||||
}
|
||||
|
||||
let _ = permissions;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
async fn set_file_permissions(_path: &std::path::Path, _permissions: Option<u32>) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1780,7 +1780,7 @@ mod serial_tests {
|
||||
/// tier again -> a second restore succeeds.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
#[serial]
|
||||
#[ignore = "global-state ILM integration test: runs serialized in the CI ILM Integration (serial) lane, see ci.yml test-ilm-integration-serial and rustfs/backlog#1148 (ilm-8)"]
|
||||
#[ignore = "global-state ILM integration test: runs serialized in the CI ILM Integration (serial) lane, see ci.yml test-ilm-integration-serial and rustfs/backlog#1148 (ilm-8); currently excluded there - DeleteRestoredAction/expire_restored delete semantics are unimplemented, so cleanup removes the whole object"]
|
||||
async fn test_restore_chain_local_read_expiry_keeps_remote_and_allows_re_restore() {
|
||||
let (_disk_paths, ecstore) = setup_test_env().await;
|
||||
|
||||
@@ -1788,7 +1788,10 @@ mod serial_tests {
|
||||
let backend = register_mock_tier(&tier_name).await;
|
||||
|
||||
let bucket_name = format!("test-restore-chain-{}", &Uuid::new_v4().simple().to_string()[..8]);
|
||||
let object_name = "restore/report.bin";
|
||||
// Must live under the `test/` prefix: `set_bucket_lifecycle_transition_with_tier`
|
||||
// installs a transition rule filtered on `test/`, so any other key never
|
||||
// transitions and `wait_for_transition` below times out.
|
||||
let object_name = "test/restore/report.bin";
|
||||
// Position-dependent payload so a misaligned read is caught.
|
||||
let payload: Vec<u8> = (0..256 * 1024).map(|i| (i % 251) as u8).collect();
|
||||
|
||||
@@ -1913,7 +1916,10 @@ mod serial_tests {
|
||||
let _backend = register_mock_tier(&tier_name).await;
|
||||
|
||||
let bucket_name = format!("test-restore-mpu3-{}", &Uuid::new_v4().simple().to_string()[..8]);
|
||||
let object_name = "restore/multipart.bin";
|
||||
// Must live under the `test/` prefix (see the transition rule filter in
|
||||
// `set_bucket_lifecycle_transition_with_tier`) or the object never
|
||||
// transitions and `wait_for_transition` below times out.
|
||||
let object_name = "test/restore/multipart.bin";
|
||||
// Three parts: 5 MiB + 5 MiB + small tail, with position-dependent
|
||||
// bytes so any part-boundary mixup is caught.
|
||||
let part_sizes = [5 * 1024 * 1024usize, 5 * 1024 * 1024, 4096];
|
||||
|
||||
+49
-10
@@ -1,13 +1,51 @@
|
||||
# RustFS Helm Mode
|
||||
|
||||
RustFS helm chart supports **standalone and distributed mode**. For standalone mode, there is only one pod and one pvc; for distributed mode, there are two styles, 4 pods and 16 pvcs(each pod has 4 pvcs), 16 pods and 16 pvcs(each pod has 1 pvc). You should decide which mode and style suits for your situation. You can specify the parameters `mode` and `replicaCount` to install different mode and style.
|
||||
RustFS helm chart supports **standalone** and **distributed** mode.
|
||||
|
||||
- **For standalone mode**: Only one pod and one pvc acts as single node single disk; Specify parameters `mode.standalone.enabled="true",mode.distributed.enabled="false"` to install.
|
||||
- **For distributed mode**(**default**): Multiple pods and multiple pvcs, acts as multiple nodes multiple disks, there are two styles:
|
||||
- 4 pods and each pods has 4 pvcs(**default**)
|
||||
- 16 pods and each pods has 1 pvc: Specify parameters `replicaCount` with `--set replicaCount="16"` to install.
|
||||
- **Standalone mode**: one pod with one PVC (single node, single disk).
|
||||
- **Distributed mode** (**default**): multiple pods with multiple PVCs (multiple nodes, multiple disks).
|
||||
|
||||
**NOTE**: Please make sure which mode suits for you situation and specify the right parameter to install rustfs on kubernetes.
|
||||
## Distributed topology
|
||||
|
||||
The distributed topology is defined by two parameters:
|
||||
|
||||
- `replicaCount` — number of pods (nodes) in the StatefulSet.
|
||||
- `drivesPerNode` — number of data PVCs mounted on each pod.
|
||||
|
||||
Total drives in the cluster = `replicaCount * drivesPerNode`.
|
||||
|
||||
When `drivesPerNode` is left unset, the chart automatically infers a
|
||||
backward-compatible value from each pool's replica count (with pools
|
||||
disabled there is a single pool driven by the top-level `replicaCount`):
|
||||
|
||||
| `replicaCount` | Inferred `drivesPerNode` | Legacy equivalent |
|
||||
|----------------|--------------------------|-------------------|
|
||||
| 4 | 4 | old default 4×4 |
|
||||
| anything else | 1 | old 16×1, etc. |
|
||||
|
||||
You can override the inference by setting `drivesPerNode` explicitly, e.g.
|
||||
`--set drivesPerNode=2` for an 8×2 cluster.
|
||||
|
||||
**IMPORTANT**: Kubernetes does **not** allow changes to
|
||||
`volumeClaimTemplates` in an existing StatefulSet. If you want to change
|
||||
`drivesPerNode` after installation you must delete the StatefulSet
|
||||
(with `--cascade=orphan` to keep pods and PVCs) and recreate it, or perform a
|
||||
full reinstall.
|
||||
|
||||
---
|
||||
|
||||
## Upgrade notes
|
||||
|
||||
Upgrading from chart versions that did **not** have `drivesPerNode` is safe
|
||||
without manual intervention:
|
||||
|
||||
- Existing 4×4 deployments (default `replicaCount=4`) continue to receive 4
|
||||
drives per node because the chart infers `drivesPerNode=4`.
|
||||
- Existing 16×1 deployments (`replicaCount=16`) continue to receive 1 drive
|
||||
per node because the chart infers `drivesPerNode=1`.
|
||||
|
||||
If you previously set `replicaCount=16` and now want a different topology,
|
||||
set both `replicaCount` and `drivesPerNode` explicitly.
|
||||
|
||||
---
|
||||
|
||||
@@ -134,7 +172,7 @@ uer. `ClusterIssuer` or `Issuer`. |
|
||||
| pdb.minAvailable | string | `""` | |
|
||||
| podAnnotations | object | `{}` | |
|
||||
| pools.enabled | bool | `false` | Enable multiple server pools (capacity expansion, distributed mode only). |
|
||||
| pools.list | list | `[]` | One entry per pool; entries may set `replicaCount` (4 or 16) and `storageclass`, omitted fields inherit top-level values. Append-only. |
|
||||
| pools.list | list | `[]` | One entry per pool; entries may set `replicaCount` (>= 2) and `storageclass`, omitted fields inherit top-level values. Append-only. |
|
||||
| podLabels | object | `{}` | |
|
||||
| podSecurityContext.fsGroup | int | `10001` | |
|
||||
| podSecurityContext.runAsGroup | int | `10001` | |
|
||||
@@ -146,7 +184,8 @@ uer. `ClusterIssuer` or `Issuer`. |
|
||||
| readinessProbe.periodSeconds | int | `5` | |
|
||||
| readinessProbe.successThreshold | int | `1` | |
|
||||
| readinessProbe.timeoutSeconds | int | `3` | |
|
||||
| replicaCount | int | `4` | Number of cluster nodes. |
|
||||
| replicaCount | int | `4` | Number of cluster nodes. Distributed mode requires >= 2. |
|
||||
| drivesPerNode | int | `null` | Number of data PVCs per pod. Inferred from replicaCount when unset (see Distributed topology above). |
|
||||
| resources.limits.cpu | string | `"200m"` | |
|
||||
| resources.limits.memory | string | `"512Mi"` | |
|
||||
| resources.requests.cpu | string | `"100m"` | |
|
||||
@@ -237,12 +276,12 @@ pools:
|
||||
list:
|
||||
- {} # pool 0: inherits top-level values and keeps the
|
||||
# existing StatefulSet/pod/PVC names and data
|
||||
- replicaCount: 4 # pool 1: new capacity (4 or 16)
|
||||
- replicaCount: 4 # pool 1: new capacity
|
||||
storageclass:
|
||||
dataStorageSize: 10Gi
|
||||
```
|
||||
|
||||
Each entry may set `replicaCount` (4 or 16) and/or a `storageclass` block;
|
||||
Each entry may set `replicaCount` (>= 2) and/or a `storageclass` block;
|
||||
omitted fields inherit the top-level values. Additional pools render as
|
||||
`<fullname>-pool<N>` StatefulSets; all pools share the headless service,
|
||||
the main service, the configuration and the credentials.
|
||||
|
||||
@@ -42,6 +42,19 @@ app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Render extra labels for the main Service resource.
|
||||
Merges (in order of increasing precedence):
|
||||
- commonLabels
|
||||
- service.labels
|
||||
*/}}
|
||||
{{- define "rustfs.serviceLabels" -}}
|
||||
{{- $labels := mergeOverwrite (dict) (default (dict) .Values.commonLabels) (default (dict) .Values.service.labels) }}
|
||||
{{- if $labels }}
|
||||
{{- toYaml $labels }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
@@ -185,6 +198,30 @@ Expects a dict with keys "root" (the chart root context) and "index".
|
||||
{{- end -}}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Return the number of data drives (PVCs) per pod for a pool.
|
||||
When .Values.drivesPerNode is set it applies to every pool. When unset the
|
||||
value is inferred per pool from its replica count so that rendered output
|
||||
stays identical to the pre-drivesPerNode chart: 4 replicas keep the legacy
|
||||
4-drive layout, everything else (16x1 and any new topology) gets one drive.
|
||||
Expects a dict with keys "root" (the chart root context) and "replicas".
|
||||
*/}}
|
||||
{{- define "rustfs.poolDrives" -}}
|
||||
{{- if kindIs "invalid" .root.Values.drivesPerNode -}}
|
||||
{{- if eq (int .replicas) 4 -}}
|
||||
4
|
||||
{{- else -}}
|
||||
1
|
||||
{{- end -}}
|
||||
{{- else -}}
|
||||
{{- $d := int .root.Values.drivesPerNode -}}
|
||||
{{- if lt $d 1 -}}
|
||||
{{- fail "drivesPerNode must be >= 1 when set" -}}
|
||||
{{- end -}}
|
||||
{{- $d -}}
|
||||
{{- end -}}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Return the normalized list of server pools as JSON.
|
||||
With pools disabled this is a single pool built from the top-level
|
||||
@@ -206,14 +243,16 @@ so entries must never be removed or reordered.
|
||||
{{- range $i, $p := .Values.pools.list -}}
|
||||
{{- $p = default (dict) $p -}}
|
||||
{{- $replicas := int (default $.Values.replicaCount $p.replicaCount) -}}
|
||||
{{- if and (ne $replicas 4) (ne $replicas 16) -}}
|
||||
{{- fail (printf "pools.list[%d].replicaCount must be 4 or 16, got %d" $i $replicas) -}}
|
||||
{{- if lt $replicas 2 -}}
|
||||
{{- fail (printf "pools.list[%d].replicaCount must be >= 2, got %d" $i $replicas) -}}
|
||||
{{- end -}}
|
||||
{{- $sc := mergeOverwrite (deepCopy $.Values.storageclass) (default (dict) $p.storageclass) -}}
|
||||
{{- $pools = append $pools (dict "index" $i "fullname" (include "rustfs.poolFullname" (dict "root" $ "index" $i)) "replicaCount" $replicas "storageclass" $sc) -}}
|
||||
{{- $drives := int (include "rustfs.poolDrives" (dict "root" $ "replicas" $replicas)) -}}
|
||||
{{- $pools = append $pools (dict "index" $i "fullname" (include "rustfs.poolFullname" (dict "root" $ "index" $i)) "replicaCount" $replicas "drives" $drives "storageclass" $sc) -}}
|
||||
{{- end -}}
|
||||
{{- else -}}
|
||||
{{- $pools = append $pools (dict "index" 0 "fullname" (include "rustfs.fullname" .) "replicaCount" (int .Values.replicaCount) "storageclass" .Values.storageclass) -}}
|
||||
{{- $drives := int (include "rustfs.poolDrives" (dict "root" $ "replicas" (int .Values.replicaCount))) -}}
|
||||
{{- $pools = append $pools (dict "index" 0 "fullname" (include "rustfs.fullname" .) "replicaCount" (int .Values.replicaCount) "drives" $drives "storageclass" .Values.storageclass) -}}
|
||||
{{- end -}}
|
||||
{{- toJson $pools -}}
|
||||
{{- end }}
|
||||
@@ -235,9 +274,10 @@ RUSTFS_VOLUMES on spaces, one pool per expression).
|
||||
{{- $exprs := list -}}
|
||||
{{- range $pool := include "rustfs.pools" . | fromJsonArray -}}
|
||||
{{- $n := int $pool.replicaCount -}}
|
||||
{{- if eq $n 4 -}}
|
||||
{{- $exprs = append $exprs (printf "%s://%s-{0...%d}.%s.%s.svc.%s:%d/data/rustfs{0...%d}" $protocol $pool.fullname (sub $n 1) $headless $ns $domain $port (sub $n 1)) -}}
|
||||
{{- else if eq $n 16 -}}
|
||||
{{- $d := int $pool.drives -}}
|
||||
{{- if gt $d 1 -}}
|
||||
{{- $exprs = append $exprs (printf "%s://%s-{0...%d}.%s.%s.svc.%s:%d/data/rustfs{0...%d}" $protocol $pool.fullname (sub $n 1) $headless $ns $domain $port (sub $d 1)) -}}
|
||||
{{- else -}}
|
||||
{{- $exprs = append $exprs (printf "%s://%s-{0...%d}.%s.%s.svc.%s:%d/data" $protocol $pool.fullname (sub $n 1) $headless $ns $domain $port) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
@@ -19,6 +19,9 @@ data:
|
||||
{{- if .domains }}
|
||||
RUSTFS_SERVER_DOMAINS: {{ include "rustfs.serverDomains" $ | quote }}
|
||||
{{- end }}
|
||||
{{- if .ec.storage_class_standard }}
|
||||
RUSTFS_STORAGE_CLASS_STANDARD: {{ .ec.storage_class_standard | quote }}
|
||||
{{- end }}
|
||||
{{- with .log_rotation }}
|
||||
{{- if .size }}
|
||||
RUSTFS_OBS_LOG_ROTATION_SIZE_MB: {{ .size | quote }}
|
||||
|
||||
@@ -131,7 +131,7 @@ spec:
|
||||
subPath: ca.crt
|
||||
- name: client-cert
|
||||
mountPath: /opt/tls/client_cert.pem
|
||||
subPath: client_cert.pem
|
||||
subPath: client_cert.pem
|
||||
- name: client-cert
|
||||
mountPath: /opt/tls/client_key.pem
|
||||
subPath: client_key.pem
|
||||
|
||||
@@ -39,8 +39,8 @@ metadata:
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "rustfs.labels" . | nindent 4 }}
|
||||
{{- with .Values.commonLabels }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- with (include "rustfs.serviceLabels" .) }}
|
||||
{{- . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if eq $serviceType "ClusterIP" }}
|
||||
@@ -64,6 +64,10 @@ spec:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- with .Values.service.externalIPs }}
|
||||
externalIPs:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- name: endpoint
|
||||
port: {{ .Values.service.endpoint.port }}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
{{- $logDir := .Values.config.rustfs.obs_log_directory }}
|
||||
{{- $logDirEnabled := ne $logDir "" }}
|
||||
{{- $poolsEnabled := and .Values.pools .Values.pools.enabled }}
|
||||
{{- if and .Values.mode.distributed.enabled (le (int .Values.replicaCount) 1) -}}
|
||||
{{- fail "Distributed mode requires replicaCount >= 2" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- if .Values.mode.distributed.enabled }}
|
||||
{{- range $pool := include "rustfs.pools" . | fromJsonArray }}
|
||||
{{- $drivesPerNode := int $pool.drives }}
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
@@ -117,17 +121,17 @@ spec:
|
||||
securityContext:
|
||||
{{- toYaml $.Values.containerSecurityContext | nindent 12 }}
|
||||
env:
|
||||
- name: REPLICA_COUNT
|
||||
value: {{ $pool.replicaCount | quote }}
|
||||
- name: DRIVES_PER_NODE
|
||||
value: {{ $drivesPerNode | quote }}
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
if [ "$REPLICA_COUNT" -eq 4 ]; then
|
||||
for i in $(seq 0 $(($REPLICA_COUNT - 1))); do
|
||||
if [ "$DRIVES_PER_NODE" -gt 1 ]; then
|
||||
for i in $(seq 0 $(($DRIVES_PER_NODE - 1))); do
|
||||
mkdir -p /data/rustfs$i
|
||||
done;
|
||||
elif [ "$REPLICA_COUNT" -eq 16 ]; then
|
||||
done
|
||||
else
|
||||
mkdir -p /data
|
||||
fi
|
||||
{{- if $logDirEnabled }}
|
||||
@@ -135,12 +139,12 @@ spec:
|
||||
chmod 755 /mnt/rustfs/logs
|
||||
{{- end }}
|
||||
volumeMounts:
|
||||
{{- if eq (int $pool.replicaCount) 4 }}
|
||||
{{- range $i := until (int $pool.replicaCount) }}
|
||||
{{- if gt $drivesPerNode 1 }}
|
||||
{{- range $i := until $drivesPerNode }}
|
||||
- name: data-rustfs-{{ $i }}
|
||||
mountPath: /data/rustfs{{ $i }}
|
||||
{{- end }}
|
||||
{{- else if eq (int $pool.replicaCount) 16 }}
|
||||
{{- else }}
|
||||
- name: data
|
||||
mountPath: /data
|
||||
{{- end }}
|
||||
@@ -201,12 +205,12 @@ spec:
|
||||
mountPath: {{ $logDir }}
|
||||
subPath: logs
|
||||
{{- end }}
|
||||
{{- if eq (int $pool.replicaCount) 4 }}
|
||||
{{- range $i := until (int $pool.replicaCount) }}
|
||||
{{- if gt $drivesPerNode 1 }}
|
||||
{{- range $i := until $drivesPerNode }}
|
||||
- name: data-rustfs-{{ $i }}
|
||||
mountPath: /data/rustfs{{ $i }}
|
||||
{{- end }}
|
||||
{{- else if eq (int $pool.replicaCount) 16 }}
|
||||
{{- else }}
|
||||
- name: data
|
||||
mountPath: /data
|
||||
{{- end }}
|
||||
@@ -246,8 +250,11 @@ spec:
|
||||
name: logs
|
||||
labels:
|
||||
{{- toYaml $.Values.commonLabels | nindent 10 }}
|
||||
{{- $logAnn := (default (dict) $pool.storageclass.pvcAnnotations).logs | default (dict) -}}
|
||||
{{- if gt (len $logAnn) 0 }}
|
||||
annotations:
|
||||
{{- toYaml $pool.storageclass.pvcAnnotations.logs | nindent 10 }}
|
||||
{{- toYaml $logAnn | nindent 10 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
accessModes: ["ReadWriteOnce"]
|
||||
storageClassName: {{ $pool.storageclass.name }}
|
||||
@@ -255,16 +262,19 @@ spec:
|
||||
requests:
|
||||
storage: {{ $pool.storageclass.logStorageSize }}
|
||||
{{- end }}
|
||||
{{- if eq (int $pool.replicaCount) 4 }}
|
||||
{{- range $i := until (int $pool.replicaCount) }}
|
||||
{{- if gt $drivesPerNode 1 }}
|
||||
{{- range $i := until $drivesPerNode }}
|
||||
- apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: data-rustfs-{{ $i }}
|
||||
labels:
|
||||
{{- toYaml $.Values.commonLabels | nindent 10 }}
|
||||
{{- $dataAnn := (default (dict) $pool.storageclass.pvcAnnotations).data | default (dict) -}}
|
||||
{{- if gt (len $dataAnn) 0 }}
|
||||
annotations:
|
||||
{{- toYaml $pool.storageclass.pvcAnnotations.data | nindent 10 }}
|
||||
{{- toYaml $dataAnn | nindent 10 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
accessModes: ["ReadWriteOnce"]
|
||||
storageClassName: {{ $pool.storageclass.name }}
|
||||
@@ -272,15 +282,18 @@ spec:
|
||||
requests:
|
||||
storage: {{ $pool.storageclass.dataStorageSize }}
|
||||
{{- end }}
|
||||
{{- else if eq (int $pool.replicaCount) 16 }}
|
||||
{{- else }}
|
||||
- apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: data
|
||||
labels:
|
||||
{{- toYaml $.Values.commonLabels | nindent 10 }}
|
||||
{{- $dataAnn := (default (dict) $pool.storageclass.pvcAnnotations).data | default (dict) -}}
|
||||
{{- if gt (len $dataAnn) 0 }}
|
||||
annotations:
|
||||
{{- toYaml $pool.storageclass.pvcAnnotations.data | nindent 10 }}
|
||||
{{- toYaml $dataAnn | nindent 10 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
accessModes: ["ReadWriteOnce"]
|
||||
storageClassName: {{ $pool.storageclass.name }}
|
||||
|
||||
+53
-18
@@ -2,9 +2,27 @@
|
||||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
|
||||
# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/
|
||||
# Number of StatefulSet pods (nodes in the distributed cluster).
|
||||
# In distributed mode this must be >= 2 (RustFS requires at least two nodes
|
||||
# for an erasure-coded cluster).
|
||||
replicaCount: 4
|
||||
|
||||
# Number of data drives (PVCs) per pod. Combined with replicaCount this
|
||||
# determines the total drive count seen by rustfs.
|
||||
# - drivesPerNode > 1: each pod gets N volumes mounted at /data/rustfs0.../data/rustfsN-1
|
||||
# - drivesPerNode = 1: each pod gets a single volume mounted at /data
|
||||
#
|
||||
# When left unset (null) the chart infers a backward-compatible value per
|
||||
# pool from that pool's replica count:
|
||||
# replicaCount = 4 -> drivesPerNode = 4 (legacy default)
|
||||
# replicaCount != 4 -> drivesPerNode = 1 (legacy 16x1 and any other topology)
|
||||
#
|
||||
# WARNING: changing drivesPerNode on an existing StatefulSet is not allowed
|
||||
# because Kubernetes forbids updates to volumeClaimTemplates. To change the
|
||||
# drive count you must delete the StatefulSet (with --cascade=orphan to keep
|
||||
# pods/PVCs) and recreate it, or perform a full reinstall.
|
||||
drivesPerNode: null
|
||||
|
||||
# Kubernetes cluster DNS domain used to build in-cluster FQDNs for
|
||||
# RUSTFS_VOLUMES (distributed mode) and mTLS server certificate SANs.
|
||||
# Override this only if your cluster does not use the default `cluster.local`
|
||||
@@ -60,7 +78,7 @@ mode:
|
||||
# With pools.enabled=true, one StatefulSet is rendered per entry of
|
||||
# pools.list and RUSTFS_VOLUMES contains every pool's volume expression
|
||||
# (space separated), which is how the server discovers multiple pools.
|
||||
# Each entry may set replicaCount (4 or 16) and/or a storageclass block;
|
||||
# Each entry may set replicaCount (>= 2) and/or a storageclass block;
|
||||
# omitted fields inherit the top-level values.
|
||||
#
|
||||
# IMPORTANT:
|
||||
@@ -99,23 +117,31 @@ config:
|
||||
# Examples
|
||||
# volumes: "/data/rustfs0,/data/rustfs1,/data/rustfs2,/data/rustfs3"
|
||||
# volumes: "http://rustfs-{0...3}.rustfs-headless:9000/data/rustfs{0...3}"
|
||||
# NOTE: When overriding volumes manually, the mount paths and drive ranges
|
||||
# must match the number of pods (replicaCount) and drives per pod
|
||||
# (drivesPerNode). Mismatched values will cause broken volume discovery.
|
||||
volumes: ""
|
||||
address: ":9000"
|
||||
console_enable: "true"
|
||||
console_address: ":9001"
|
||||
log_level: "info"
|
||||
region: "us-east-1"
|
||||
obs_log_directory: "/logs" # Set to "" to disable log PVCs and mounts.
|
||||
obs_environment: "development"
|
||||
# Optionally enable support for virtual-hosted-style requests.
|
||||
# See more information: https://docs.rustfs.com/integration/virtual.html
|
||||
domains: "" # e.g. "example.com"
|
||||
ec:
|
||||
storage_class_standard: "EC:4" # Storage class for standard storage class in erasure coding mode, default is "STANDARD"
|
||||
log_rotation: # Specify log rotation settings
|
||||
# size: 100 # Default value: 100 MB
|
||||
# time: hour # Default value: hour, eg: day,hour,minute,second
|
||||
# keep_files: 30 # number of rotated log files to keep
|
||||
# Erasure-coding parity for the STANDARD storage class, e.g. "EC:4".
|
||||
# Left empty by default: the server then picks a parity suited to the
|
||||
# drive count. If you set it, it must be valid for the total drive
|
||||
# count (total drives = replicaCount * drivesPerNode):
|
||||
#
|
||||
# | replicaCount | drivesPerNode | Total drives | Valid parity |
|
||||
# | ------------ | ------------- | ------------ | ------------ |
|
||||
# | 4 | 1 | 4 | 0-2 |
|
||||
# | 4 | 2 | 8 | 0-4 |
|
||||
# | 4 | 4 | 16 | 0-8 |
|
||||
# | 8 | 1 | 8 | 0-4 |
|
||||
# | 16 | 1 | 16 | 0-8 |
|
||||
storage_class_standard: ""
|
||||
scanner:
|
||||
# Scanner speed preset: fastest|fast|default|slow|slowest
|
||||
speed: ""
|
||||
@@ -155,6 +181,16 @@ config:
|
||||
# - default: keep current timeout defaults.
|
||||
# - high_latency: use higher timeout defaults for scanner-sensitive operations.
|
||||
timeout_profile: ""
|
||||
log_level: "info"
|
||||
log_rotation: # Specify log rotation settings
|
||||
# size: 100 # Default value: 100 MB
|
||||
# time: hour # Default value: hour, eg: day,hour,minute,second
|
||||
# keep_files: 30 # number of rotated log files to keep
|
||||
## If obs_log_directory is not empty, a separate volume for logs will be created and the logs will NOT be sent to stdout. If not set, will default to default chart value "/logs"
|
||||
obs_log_directory: "/logs"
|
||||
## Supported values include `production`, `development`, `test`, and `staging`;
|
||||
## refer to the RustFS configuration documentation/source for the authoritative list.
|
||||
obs_environment: "development"
|
||||
obs_endpoint:
|
||||
enabled: false # If true, rustfs will export metrics, traces, logs and profiling data to the specified OTLP endpoints. If false, the individual settings for metrics, traces, logs and profiling endpoints will be ignored and all data will not be exported.
|
||||
base_endpoint: "" #Root OTLP/HTTP endpoint, e.g. http://otel-collector:4318
|
||||
@@ -175,7 +211,7 @@ config:
|
||||
enabled: false
|
||||
type: "vault" # Only Support vault currently.
|
||||
vault:
|
||||
vault_backend: "" # Only support vault kv2 and vault transit.
|
||||
vault_backend: "" # Only support vault kv2 and vault transit.
|
||||
vault_address: ""
|
||||
vault_token: ""
|
||||
vault_mount_path: ""
|
||||
@@ -235,6 +271,7 @@ priorityClassName: ""
|
||||
|
||||
service:
|
||||
annotations: {}
|
||||
labels: {} # Additional labels
|
||||
headlessAnnotations: {} # Applied to the headless Service when mode.distributed.enabled=true
|
||||
traefikAnnotations: # Applied to the Service when mode.distributed.enabled=true and ingress.className=traefik
|
||||
traefik.ingress.kubernetes.io/service.sticky.cookie: "true"
|
||||
@@ -247,6 +284,7 @@ service:
|
||||
loadBalancerIP: "" # Request a specific IP from the load balancer (e.g. for Cilium LB-IPAM or MetalLB)
|
||||
loadBalancerClass: "" # Specify a load balancer implementation (e.g. "io.cilium/bgp", "metallb")
|
||||
loadBalancerSourceRanges: [] # Restrict client IPs (e.g. ["10.0.0.0/8"])
|
||||
externalIPs: [] # Expose service via specific external IPs (e.g. ["203.0.113.1"])
|
||||
endpoint:
|
||||
port: 9000
|
||||
nodePort: 32000
|
||||
@@ -297,7 +335,7 @@ ingress:
|
||||
gatewayApi:
|
||||
enabled: false
|
||||
gatewayClass: traefik # Only support for traefik and contour gatewayClass at the moment.
|
||||
listeners: # Specify which listeners to create on the Gateway.
|
||||
listeners: # Specify which listeners to create on the Gateway.
|
||||
http:
|
||||
name: web
|
||||
port: 8000
|
||||
@@ -380,12 +418,9 @@ storageclass:
|
||||
name: local-path
|
||||
dataStorageSize: 256Mi
|
||||
logStorageSize: 256Mi
|
||||
pvcAnnotations: {}
|
||||
#pvcAnnotations:
|
||||
# data:
|
||||
# key: value
|
||||
# logs:
|
||||
# key: value
|
||||
pvcAnnotations:
|
||||
data: {}
|
||||
logs: {}
|
||||
|
||||
pdb:
|
||||
create: false
|
||||
|
||||
@@ -2034,7 +2034,8 @@ fn table_entry_from_create_table_request(
|
||||
let table_uuid = Uuid::new_v4().to_string();
|
||||
let warehouse_location = request.location.unwrap_or_else(|| format!("s3://{bucket}/tables/{table_id}"));
|
||||
validate_table_location_in_bucket(bucket, &warehouse_location)?;
|
||||
let metadata_location = crate::table_catalog::default_table_metadata_file_path(namespace, &table, "00001.metadata.json");
|
||||
let metadata_location =
|
||||
crate::table_catalog::default_table_metadata_file_path(namespace, &table, &next_metadata_file_name(1, &table_id));
|
||||
|
||||
let mut entry = crate::table_catalog::TableEntry {
|
||||
version: crate::table_catalog::TABLE_CATALOG_ENTRY_VERSION,
|
||||
@@ -6890,10 +6891,6 @@ mod tests {
|
||||
.await
|
||||
.expect("table should be created");
|
||||
|
||||
assert_eq!(
|
||||
response.metadata_location,
|
||||
"s3://warehouse/.rustfs-table/warehouses/default/namespaces/analytics/tables/events/metadata/00001.metadata.json"
|
||||
);
|
||||
assert_eq!(response.metadata["format-version"], 2);
|
||||
assert_eq!(response.metadata["current-schema-id"], 0);
|
||||
assert_eq!(response.metadata["default-spec-id"], 0);
|
||||
@@ -6907,6 +6904,13 @@ mod tests {
|
||||
.await
|
||||
.expect("table lookup should succeed")
|
||||
.expect("table should exist");
|
||||
assert_eq!(
|
||||
response.metadata_location,
|
||||
format!(
|
||||
"s3://warehouse/.rustfs-table/warehouses/default/namespaces/analytics/tables/events/metadata/00001-{}.metadata.json",
|
||||
entry.table_id
|
||||
)
|
||||
);
|
||||
assert_eq!(response.metadata["table-uuid"], entry.table_uuid);
|
||||
assert!(
|
||||
metadata_backend
|
||||
@@ -6916,6 +6920,212 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_table_response_recreates_dropped_identifier_without_overwriting_retained_metadata() {
|
||||
let store = TestTableCatalogStore::default();
|
||||
let metadata_backend = TestTableCatalogObjectBackend::default();
|
||||
let namespace = crate::table_catalog::Namespace::parse("analytics").expect("namespace should parse");
|
||||
create_standard_events_table(&store, &metadata_backend, &namespace).await;
|
||||
let first_entry = store
|
||||
.load_table("warehouse", "analytics", "events")
|
||||
.await
|
||||
.expect("first table lookup should succeed")
|
||||
.expect("first table should exist");
|
||||
let retained_initial = metadata_backend
|
||||
.read_object("warehouse", &first_entry.metadata_location)
|
||||
.await
|
||||
.expect("first metadata lookup should succeed")
|
||||
.expect("first metadata should exist");
|
||||
|
||||
let commit_request: RestCommitTableRequest = serde_json::from_value(serde_json::json!({
|
||||
"updates": [
|
||||
{
|
||||
"action": "set-properties",
|
||||
"updates": {
|
||||
"owner": "first-table"
|
||||
}
|
||||
}
|
||||
]
|
||||
}))
|
||||
.expect("standard commit request should parse");
|
||||
standard_commit_table_response(&store, &metadata_backend, "warehouse", &namespace, "events", commit_request)
|
||||
.await
|
||||
.expect("first table metadata commit should succeed");
|
||||
let first_committed_entry = store
|
||||
.load_table("warehouse", "analytics", "events")
|
||||
.await
|
||||
.expect("committed table lookup should succeed")
|
||||
.expect("committed table should exist");
|
||||
|
||||
drop_table_in_store(&store, "warehouse", &namespace, "events")
|
||||
.await
|
||||
.expect("first table should drop");
|
||||
drop_namespace_in_store(&store, "warehouse", "analytics")
|
||||
.await
|
||||
.expect("first namespace should drop");
|
||||
create_namespace_response(
|
||||
&store,
|
||||
"warehouse",
|
||||
CreateNamespaceRequest {
|
||||
namespace: vec!["analytics".to_string()],
|
||||
properties: BTreeMap::new(),
|
||||
},
|
||||
true,
|
||||
)
|
||||
.await
|
||||
.expect("namespace should be recreated");
|
||||
|
||||
let create_request: CreateTableRequest = serde_json::from_value(serde_json::json!({
|
||||
"name": "events",
|
||||
"schema": {
|
||||
"type": "struct",
|
||||
"schema-id": 0,
|
||||
"fields": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"type": "long"
|
||||
}
|
||||
]
|
||||
}
|
||||
}))
|
||||
.expect("recreate table request should parse");
|
||||
let second = create_table_response(&store, &metadata_backend, "warehouse", &namespace, create_request, true)
|
||||
.await
|
||||
.expect("dropped table identifier should be reusable");
|
||||
let second_entry = store
|
||||
.load_table("warehouse", "analytics", "events")
|
||||
.await
|
||||
.expect("recreated table lookup should succeed")
|
||||
.expect("recreated table should exist");
|
||||
|
||||
assert_ne!(first_entry.table_id, second_entry.table_id);
|
||||
assert_ne!(first_entry.table_uuid, second_entry.table_uuid);
|
||||
assert_ne!(first_entry.metadata_location, second_entry.metadata_location);
|
||||
assert_ne!(first_committed_entry.metadata_location, second_entry.metadata_location);
|
||||
assert_eq!(second.metadata["table-uuid"], second_entry.table_uuid);
|
||||
assert_eq!(second.metadata["metadata-log"], serde_json::json!([]));
|
||||
assert_eq!(
|
||||
metadata_backend
|
||||
.read_object("warehouse", &first_entry.metadata_location)
|
||||
.await
|
||||
.expect("retained metadata lookup should succeed")
|
||||
.expect("retained metadata should still exist")
|
||||
.data,
|
||||
retained_initial.data
|
||||
);
|
||||
assert!(
|
||||
metadata_backend
|
||||
.object_exists("warehouse", &first_committed_entry.metadata_location)
|
||||
.await
|
||||
.expect("committed metadata lookup should succeed")
|
||||
);
|
||||
assert!(
|
||||
metadata_backend
|
||||
.object_exists("warehouse", &second_entry.metadata_location)
|
||||
.await
|
||||
.expect("recreated metadata lookup should succeed")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn concurrent_create_table_responses_keep_one_catalog_winner_with_distinct_metadata() {
|
||||
let catalog_backend = TestTableCatalogObjectBackend::default();
|
||||
let store = crate::table_catalog::ObjectTableCatalogStore::new(catalog_backend);
|
||||
let metadata_backend = TestTableCatalogObjectBackend {
|
||||
objects: Arc::new(tokio::sync::Mutex::new(BTreeMap::new())),
|
||||
put_object_barrier: Some(Arc::new(tokio::sync::Barrier::new(2))),
|
||||
};
|
||||
let namespace = crate::table_catalog::Namespace::parse("analytics").expect("namespace should parse");
|
||||
ensure_table_bucket_entry(&store, "warehouse", true)
|
||||
.await
|
||||
.expect("table bucket entry should be seeded");
|
||||
create_namespace_response(
|
||||
&store,
|
||||
"warehouse",
|
||||
CreateNamespaceRequest {
|
||||
namespace: vec!["analytics".to_string()],
|
||||
properties: BTreeMap::new(),
|
||||
},
|
||||
true,
|
||||
)
|
||||
.await
|
||||
.expect("namespace should be created");
|
||||
let create_request = || {
|
||||
serde_json::from_value::<CreateTableRequest>(serde_json::json!({
|
||||
"name": "events",
|
||||
"schema": {
|
||||
"type": "struct",
|
||||
"schema-id": 0,
|
||||
"fields": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"type": "long"
|
||||
}
|
||||
]
|
||||
}
|
||||
}))
|
||||
.expect("concurrent create table request should parse")
|
||||
};
|
||||
|
||||
let (first, second) = tokio::join!(
|
||||
create_table_response(&store, &metadata_backend, "warehouse", &namespace, create_request(), true,),
|
||||
create_table_response(&store, &metadata_backend, "warehouse", &namespace, create_request(), true,)
|
||||
);
|
||||
assert_ne!(first.is_ok(), second.is_ok(), "exactly one concurrent create should succeed");
|
||||
let (winner, loser) = match (first, second) {
|
||||
(Ok(winner), Err(loser)) | (Err(loser), Ok(winner)) => (winner, loser),
|
||||
_ => unreachable!("success count was checked above"),
|
||||
};
|
||||
assert!(format!("{loser:?}").contains("AlreadyExistsException"));
|
||||
|
||||
let tables = store
|
||||
.list_tables("warehouse", "analytics")
|
||||
.await
|
||||
.expect("table listing should succeed");
|
||||
assert_eq!(tables.len(), 1);
|
||||
let winner_entry = &tables[0];
|
||||
assert_eq!(
|
||||
winner.metadata_location,
|
||||
table_metadata_location_for_client("warehouse", &winner_entry.metadata_location)
|
||||
);
|
||||
let metadata_prefix = winner_entry
|
||||
.metadata_location
|
||||
.rsplit_once('/')
|
||||
.map(|(prefix, _)| format!("{prefix}/"))
|
||||
.expect("metadata location should contain a file name");
|
||||
let metadata_objects = metadata_backend
|
||||
.list_objects("warehouse", &metadata_prefix)
|
||||
.await
|
||||
.expect("metadata listing should succeed");
|
||||
assert_eq!(metadata_objects.len(), 2);
|
||||
assert_ne!(metadata_objects[0], metadata_objects[1]);
|
||||
|
||||
let mut table_uuids = Vec::with_capacity(metadata_objects.len());
|
||||
for metadata_object in metadata_objects {
|
||||
let metadata = metadata_backend
|
||||
.read_object("warehouse", &metadata_object)
|
||||
.await
|
||||
.expect("metadata lookup should succeed")
|
||||
.expect("metadata object should exist");
|
||||
let metadata: serde_json::Value =
|
||||
serde_json::from_slice(&metadata.data).expect("metadata object should contain JSON");
|
||||
table_uuids.push(
|
||||
metadata["table-uuid"]
|
||||
.as_str()
|
||||
.expect("metadata should contain table uuid")
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
table_uuids.sort();
|
||||
table_uuids.dedup();
|
||||
assert_eq!(table_uuids.len(), 2);
|
||||
assert!(table_uuids.contains(&winner_entry.table_uuid));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn standard_commit_applies_updates_and_writes_next_metadata() {
|
||||
let store = TestTableCatalogStore::default();
|
||||
@@ -6991,10 +7201,7 @@ mod tests {
|
||||
assert_eq!(commit.metadata["current-snapshot-id"], 10);
|
||||
assert_eq!(commit.metadata["last-sequence-number"], 1);
|
||||
assert_eq!(commit.metadata["refs"]["main"]["snapshot-id"], 10);
|
||||
assert_eq!(
|
||||
commit.metadata["metadata-log"][0]["metadata-file"],
|
||||
"s3://warehouse/.rustfs-table/warehouses/default/namespaces/analytics/tables/events/metadata/00001.metadata.json"
|
||||
);
|
||||
assert_eq!(commit.metadata["metadata-log"][0]["metadata-file"], created.metadata_location);
|
||||
let committed = store
|
||||
.load_table("warehouse", "analytics", "events")
|
||||
.await
|
||||
|
||||
@@ -1530,9 +1530,20 @@ async fn put_bucket_lifecycle_configuration_rejects_zero_day_expiration() {
|
||||
/// copy-back completes the object reports `ongoing-request="false"` with a
|
||||
/// future expiry-date; and a full GET is then served from the local restored
|
||||
/// copy (the mock tier records no further `get` calls).
|
||||
///
|
||||
/// CURRENTLY EXCLUDED from the CI ILM Integration (serial) lane (see ci.yml):
|
||||
/// the #4877 restore self-deadlock is now fixed, so restore completes, but this
|
||||
/// test asserts a concurrent `get_object_info` observes `ongoing-request="true"`
|
||||
/// mid-restore. #4877 serializes reads against the restore write lock, so the
|
||||
/// concurrent read only returns once the copy-back has finished and already
|
||||
/// cleared the ongoing flag — it reads `false`, failing the assertion. Whether
|
||||
/// a concurrent restore should instead reject fast (`ErrObjectRestoreAlreadyInProgress`)
|
||||
/// while keeping HEAD non-blocking (backlog#1148 ilm-8 criterion 1) is an
|
||||
/// API-semantics decision, not a bug; revisit before re-enabling. The `test/`
|
||||
/// prefix and the rest of the contract are correct.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
#[serial]
|
||||
#[ignore = "global-state ILM integration test: runs serialized in the CI ILM Integration (serial) lane, see ci.yml test-ilm-integration-serial and rustfs/backlog#1148 (ilm-8)"]
|
||||
#[ignore = "global-state ILM integration test: runs serialized in the CI ILM Integration (serial) lane, see ci.yml test-ilm-integration-serial and rustfs/backlog#1148 (ilm-8); currently excluded there — asserts a concurrent ongoing-request=true read that #4877's read-vs-restore serialization rules out (API-semantics decision)"]
|
||||
async fn restore_object_usecase_reports_ongoing_conflict_and_completion() {
|
||||
let (_disk_paths, ecstore) = setup_test_env().await;
|
||||
let usecase = DefaultObjectUsecase::from_global();
|
||||
|
||||
@@ -303,3 +303,70 @@ if grep -q 'svc\.cluster\.local"' <<<"$cert_custom"; then
|
||||
echo "Custom clusterDomain must fully replace cluster.local in mTLS server cert SANs" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Legacy topology compatibility: default replicaCount=4 (no drivesPerNode set)
|
||||
# must render the old 4x4 PVC names (data-rustfs-0 .. data-rustfs-3).
|
||||
legacy_four_by_four=$(render_distributed_statefulset)
|
||||
for i in 0 1 2 3; do
|
||||
if ! grep -q "name: data-rustfs-${i}" <<<"$legacy_four_by_four"; then
|
||||
echo "Legacy 4x4 topology must contain PVC data-rustfs-${i}" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
if grep -q "name: data$" <<<"$legacy_four_by_four"; then
|
||||
echo "Legacy 4x4 topology must NOT contain a single 'data' PVC" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Legacy topology compatibility: replicaCount=16 (no drivesPerNode set)
|
||||
# must render a single 'data' PVC (old 16x1 behaviour).
|
||||
legacy_sixteen_by_one=$(render_distributed_statefulset --set replicaCount=16)
|
||||
if ! grep -q "name: data$" <<<"$legacy_sixteen_by_one"; then
|
||||
echo "Legacy 16x1 topology must contain a single 'data' PVC" >&2
|
||||
exit 1
|
||||
fi
|
||||
if grep -q "name: data-rustfs-" <<<"$legacy_sixteen_by_one"; then
|
||||
echo "Legacy 16x1 topology must NOT contain data-rustfs-* PVCs" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Generic topology: explicit replicaCount=8 drivesPerNode=2 must render
|
||||
# exactly two PVCs per pod.
|
||||
generic_eight_by_two=$(render_distributed_statefulset --set replicaCount=8 --set drivesPerNode=2)
|
||||
for i in 0 1; do
|
||||
if ! grep -q "name: data-rustfs-${i}" <<<"$generic_eight_by_two"; then
|
||||
echo "Generic 8x2 topology must contain PVC data-rustfs-${i}" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
if grep -q "name: data$" <<<"$generic_eight_by_two"; then
|
||||
echo "Generic 8x2 topology must NOT contain a single 'data' PVC" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# volumeClaimTemplates must not contain empty annotations when pvcAnnotations are unset,
|
||||
# because Kubernetes treats annotations: {} as a mutation of the immutable field.
|
||||
no_ann_output=$(render_distributed_statefulset)
|
||||
if grep -A1 'kind: PersistentVolumeClaim' <<<"$no_ann_output" | grep -q 'annotations:'; then
|
||||
echo "Empty pvcAnnotations must not render an annotations key in volumeClaimTemplates" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Distributed mode with replicaCount < 2 must fail rendering.
|
||||
low_replica_status=0
|
||||
render_distributed_statefulset --set replicaCount=1 >/dev/null 2>&1 || low_replica_status=$?
|
||||
if [[ $low_replica_status -eq 0 ]]; then
|
||||
echo "Distributed mode with replicaCount=1 must fail rendering" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# service.externalIPs must render correctly when supplied.
|
||||
external_ips_output=$(helm template rustfs "$CHART_DIR" \
|
||||
--namespace rustfs \
|
||||
--set secret.rustfs.access_key=test-access-key \
|
||||
--set secret.rustfs.secret_key=test-secret-key \
|
||||
--set 'service.externalIPs[0]=203.0.113.1')
|
||||
if ! grep -A2 'externalIPs:' <<<"$external_ips_output" | grep -q '203.0.113.1'; then
|
||||
echo "service.externalIPs must contain 203.0.113.1" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user