mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-11 09:05:50 +00:00
Compare commits
31
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f5780d1ba | ||
|
|
c2ba65053e | ||
|
|
06ea831368 | ||
|
|
f3376c1af0 | ||
|
|
6ad8cb56f7 | ||
|
|
2b184ed0b8 | ||
|
|
acbb483b31 | ||
|
|
4f05f657de | ||
|
|
7e0664aee9 | ||
|
|
33e400eb88 | ||
|
|
1598c4b8b9 | ||
|
|
988e6d1a2d | ||
|
|
324a292f9c | ||
|
|
5e258a5ce7 | ||
|
|
f25f573816 | ||
|
|
3ebbd25a4b | ||
|
|
85369414f9 | ||
|
|
522ca206be | ||
|
|
c5af6a82c7 | ||
|
|
a2e77cb433 | ||
|
|
34aced3526 | ||
|
|
2c26aea9c3 | ||
|
|
ccc0a20dbf | ||
|
|
9b66850ec0 | ||
|
|
8d40aceb48 | ||
|
|
85d90e430e | ||
|
|
fa94484a5b | ||
|
|
ca86793453 | ||
|
|
27b6c30847 | ||
|
|
888e71ec9b | ||
|
|
fa84efbf7f |
@@ -223,23 +223,5 @@ jobs:
|
||||
|
||||
echo "✓ Successfully copied ${{ matrix.variant }} to Docker Hub"
|
||||
|
||||
helm-release:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [copy-to-dockerhub]
|
||||
permissions:
|
||||
contents: write
|
||||
pages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Publish Helm charts
|
||||
uses: stefanprodan/helm-gh-pages@v1.7.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
charts_dir: k8s/charts
|
||||
target_dir: helm
|
||||
branch: gh-pages
|
||||
helm_version: "3.18.4"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -33,29 +33,9 @@ jobs:
|
||||
run: |
|
||||
go test -v
|
||||
|
||||
- name: Collect server logs on failure
|
||||
if: failure()
|
||||
run: |
|
||||
echo "Collecting server logs from temp directories..."
|
||||
mkdir -p /tmp/ec-test-logs
|
||||
# Find all temp directories created by the tests (they persist on failure with t.TempDir())
|
||||
find /tmp -maxdepth 1 -type d \( -name "TestEC*" -o -name "TestDisk*" -o -name "TestCross*" -o -name "TestEvacuation*" \) 2>/dev/null | while read dir; do
|
||||
if [ -d "$dir" ]; then
|
||||
echo "Found test directory: $dir"
|
||||
# Copy the entire directory structure to preserve organization
|
||||
cp -r "$dir" /tmp/ec-test-logs/ 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
# List what we collected
|
||||
echo "Collected logs:"
|
||||
find /tmp/ec-test-logs -type f -name "*.log" 2>/dev/null || echo "No logs found"
|
||||
|
||||
- name: Archive logs
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ec-integration-test-logs
|
||||
path: |
|
||||
/tmp/ec-test-logs/
|
||||
test/erasure_coding/
|
||||
if-no-files-found: warn
|
||||
path: test/erasure_coding
|
||||
@@ -33,15 +33,6 @@ jobs:
|
||||
run: |
|
||||
cd weed; go get -v -t -d ./...
|
||||
|
||||
- name: Go Vet (excluding protobuf lock copying)
|
||||
run: |
|
||||
cd weed
|
||||
# Run go vet and filter out known protobuf MessageState lock copying warnings
|
||||
# These are expected in generated protobuf code with embedded sync.Mutex and are safe in practice
|
||||
go vet -v ./... 2>&1 | grep -v "MessageState contains sync.Mutex" | grep -v "IdentityAccessManagement contains sync.RWMutex" | tee vet-output.txt
|
||||
# Fail only if there are actual vet errors (not counting the filtered lock warnings)
|
||||
if grep -q "vet:" vet-output.txt; then exit 1; fi
|
||||
|
||||
- name: Build
|
||||
run: cd weed; go build -tags "elastic gocdk sqlite ydb tarantool tikv rclone" -v .
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
name: "helm: publish charts"
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pages: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
- name: Publish Helm charts
|
||||
uses: stefanprodan/helm-gh-pages@v1.7.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
charts_dir: k8s/charts
|
||||
target_dir: helm
|
||||
branch: gh-pages
|
||||
helm_version: "3.18.4"
|
||||
@@ -52,7 +52,6 @@ jobs:
|
||||
run: |
|
||||
cd test/kafka
|
||||
go mod download
|
||||
go mod tidy
|
||||
|
||||
- name: Run Kafka Gateway Unit Tests
|
||||
run: |
|
||||
@@ -97,7 +96,6 @@ jobs:
|
||||
run: |
|
||||
cd test/kafka
|
||||
go mod download
|
||||
go mod tidy
|
||||
|
||||
- name: Run Integration Tests
|
||||
run: |
|
||||
@@ -156,7 +154,6 @@ jobs:
|
||||
cd test/kafka
|
||||
# Use go mod download with timeout to prevent hanging
|
||||
timeout 90s go mod download || echo "Warning: Dependency download timed out, continuing with cached modules"
|
||||
go mod tidy
|
||||
|
||||
- name: Build and start SeaweedFS MQ
|
||||
run: |
|
||||
@@ -335,7 +332,6 @@ jobs:
|
||||
cd test/kafka
|
||||
# Use go mod download with timeout to prevent hanging
|
||||
timeout 90s go mod download || echo "Warning: Dependency download timed out, continuing with cached modules"
|
||||
go mod tidy
|
||||
|
||||
- name: Build and start SeaweedFS MQ
|
||||
run: |
|
||||
@@ -496,7 +492,6 @@ jobs:
|
||||
run: |
|
||||
cd test/kafka
|
||||
timeout 90s go mod download || echo "Warning: Dependency download timed out, continuing with cached modules"
|
||||
go mod tidy
|
||||
|
||||
- name: Build and start SeaweedFS MQ
|
||||
run: |
|
||||
@@ -654,7 +649,6 @@ jobs:
|
||||
run: |
|
||||
cd test/kafka
|
||||
timeout 90s go mod download || echo "Warning: Dependency download timed out, continuing with cached modules"
|
||||
go mod tidy
|
||||
|
||||
- name: Build and start SeaweedFS MQ
|
||||
run: |
|
||||
@@ -809,7 +803,6 @@ jobs:
|
||||
run: |
|
||||
cd test/kafka
|
||||
go mod download
|
||||
go mod tidy
|
||||
|
||||
- name: Run Protocol Tests
|
||||
run: |
|
||||
|
||||
@@ -345,53 +345,3 @@ jobs:
|
||||
name: s3-sse-performance-logs
|
||||
path: test/s3/sse/weed-test*.log
|
||||
retention-days: 7
|
||||
|
||||
s3-volume-encryption:
|
||||
name: S3 Volume Encryption Test
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 20
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
id: go
|
||||
|
||||
- name: Install SeaweedFS
|
||||
run: |
|
||||
go install -buildvcs=false
|
||||
|
||||
- name: Run S3 Volume Encryption Integration Tests
|
||||
timeout-minutes: 15
|
||||
working-directory: test/s3/sse
|
||||
run: |
|
||||
set -x
|
||||
echo "=== System Information ==="
|
||||
uname -a
|
||||
free -h
|
||||
|
||||
# Run volume encryption tests with -s3.encryptVolumeData flag
|
||||
echo "🚀 Running S3 volume encryption integration tests..."
|
||||
make test-volume-encryption || {
|
||||
echo "❌ Volume encryption tests failed, checking logs..."
|
||||
if [ -f /tmp/seaweedfs-sse-mini.log ]; then
|
||||
echo "=== Server logs ==="
|
||||
tail -100 /tmp/seaweedfs-sse-mini.log
|
||||
fi
|
||||
echo "=== Process information ==="
|
||||
ps aux | grep -E "(weed|test)" || true
|
||||
exit 1
|
||||
}
|
||||
|
||||
- name: Upload server logs on failure
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: s3-volume-encryption-logs
|
||||
path: /tmp/seaweedfs-sse-*.log
|
||||
retention-days: 3
|
||||
|
||||
|
||||
@@ -131,9 +131,3 @@ coverage.out
|
||||
test/s3/remote_cache/remote-server.pid
|
||||
test/s3/remote_cache/primary-server.pid
|
||||
/test/erasure_coding/filerldb2
|
||||
/test/s3/cors/test-mini-data
|
||||
/test/s3/filer_group/test-volume-data
|
||||
|
||||
# ID and PID files
|
||||
*.id
|
||||
*.pid
|
||||
|
||||
@@ -54,9 +54,9 @@ Table of Contents
|
||||
=================
|
||||
|
||||
* [Quick Start](#quick-start)
|
||||
* [Quick Start with weed mini](#quick-start-with-weed-mini)
|
||||
* [Quick Start for S3 API on Docker](#quick-start-for-s3-api-on-docker)
|
||||
* [Quick Start with Single Binary](#quick-start-with-single-binary)
|
||||
* [Quick Start SeaweedFS S3 on AWS](#quick-start-seaweedfs-s3-on-aws)
|
||||
* [Introduction](#introduction)
|
||||
* [Features](#features)
|
||||
* [Additional Features](#additional-features)
|
||||
@@ -78,32 +78,6 @@ Table of Contents
|
||||
|
||||
# Quick Start #
|
||||
|
||||
|
||||
## Quick Start with weed mini ##
|
||||
The easiest way to get started with SeaweedFS for development and testing:
|
||||
|
||||
* Download the latest binary from https://github.com/seaweedfs/seaweedfs/releases and unzip a single binary file `weed` or `weed.exe`.
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
# remove quarantine on macOS
|
||||
# xattr -d com.apple.quarantine ./weed
|
||||
export AWS_ACCESS_KEY_ID=admin
|
||||
export AWS_SECRET_ACCESS_KEY=key
|
||||
./weed mini -dir=/data
|
||||
```
|
||||
|
||||
This single command starts a complete SeaweedFS setup with:
|
||||
- **Master UI**: http://localhost:9333
|
||||
- **Volume Server**: http://localhost:9340
|
||||
- **Filer UI**: http://localhost:8888
|
||||
- **S3 Endpoint**: http://localhost:8333
|
||||
- **WebDAV**: http://localhost:7333
|
||||
- **Admin UI**: http://localhost:23646
|
||||
|
||||
Perfect for development, testing, learning SeaweedFS, and single node deployments!
|
||||
|
||||
## Quick Start for S3 API on Docker ##
|
||||
|
||||
`docker run -p 8333:8333 chrislusf/seaweedfs server -s3`
|
||||
@@ -111,10 +85,13 @@ Perfect for development, testing, learning SeaweedFS, and single node deployment
|
||||
## Quick Start with Single Binary ##
|
||||
* Download the latest binary from https://github.com/seaweedfs/seaweedfs/releases and unzip a single binary file `weed` or `weed.exe`. Or run `go install github.com/seaweedfs/seaweedfs/weed@latest`.
|
||||
* `export AWS_ACCESS_KEY_ID=admin ; export AWS_SECRET_ACCESS_KEY=key` as the admin credentials to access the object store.
|
||||
* Run `weed server -dir=/some/data/dir -s3` to start one master, one volume server, one filer, and one S3 gateway. The difference with `weed mini` is that `weed mini` can auto configure based on the single host environment, while `weed server` requires manual configuration and are designed for production use.
|
||||
* Run `weed server -dir=/some/data/dir -s3` to start one master, one volume server, one filer, and one S3 gateway.
|
||||
|
||||
Also, to increase capacity, just add more volume servers by running `weed volume -dir="/some/data/dir2" -master="<master_host>:9333" -port=8081` locally, or on a different machine, or on thousands of machines. That is it!
|
||||
|
||||
## Quick Start SeaweedFS S3 on AWS ##
|
||||
* Setup fast production-ready [SeaweedFS S3 on AWS with cloudformation](https://aws.amazon.com/marketplace/pp/prodview-nzelz5gprlrjc)
|
||||
|
||||
# Introduction #
|
||||
|
||||
SeaweedFS is a simple and highly scalable distributed file system. There are two objectives:
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
# Server-Side Encryption with Customer-Provided Keys (SSE-C) Implementation
|
||||
|
||||
This document describes the implementation of SSE-C support in SeaweedFS, addressing the feature request from [GitHub Discussion #5361](https://github.com/seaweedfs/seaweedfs/discussions/5361).
|
||||
|
||||
## Overview
|
||||
|
||||
SSE-C allows clients to provide their own encryption keys for server-side encryption of objects stored in SeaweedFS. The server encrypts the data using the customer-provided AES-256 key but does not store the key itself - only an MD5 hash of the key for validation purposes.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Architecture
|
||||
|
||||
The SSE-C implementation follows a transparent encryption/decryption pattern:
|
||||
|
||||
1. **Upload (PUT/POST)**: Data is encrypted with the customer key before being stored
|
||||
2. **Download (GET/HEAD)**: Encrypted data is decrypted on-the-fly using the customer key
|
||||
3. **Metadata Storage**: Only the encryption algorithm and key MD5 are stored as metadata
|
||||
|
||||
### Key Components
|
||||
|
||||
#### 1. Constants and Headers (`weed/s3api/s3_constants/header.go`)
|
||||
- Added AWS-compatible SSE-C header constants
|
||||
- Support for both regular and copy-source SSE-C headers
|
||||
|
||||
#### 2. Core SSE-C Logic (`weed/s3api/s3_sse_c.go`)
|
||||
- **SSECustomerKey**: Structure to hold customer encryption key and metadata
|
||||
- **SSECEncryptedReader**: Streaming encryption with AES-256-CTR mode
|
||||
- **SSECDecryptedReader**: Streaming decryption with IV extraction
|
||||
- **validateAndParseSSECHeaders**: Shared validation logic (DRY principle)
|
||||
- **ParseSSECHeaders**: Parse regular SSE-C headers
|
||||
- **ParseSSECCopySourceHeaders**: Parse copy-source SSE-C headers
|
||||
- Header validation and parsing functions
|
||||
- Metadata extraction and response handling
|
||||
|
||||
#### 3. Error Handling (`weed/s3api/s3err/s3api_errors.go`)
|
||||
- New error codes for SSE-C validation failures
|
||||
- AWS-compatible error messages and HTTP status codes
|
||||
|
||||
#### 4. S3 API Integration
|
||||
- **PUT Object Handler**: Encrypts data streams transparently
|
||||
- **GET Object Handler**: Decrypts data streams transparently
|
||||
- **HEAD Object Handler**: Validates keys and returns appropriate headers
|
||||
- **Metadata Storage**: Integrates with existing `SaveAmzMetaData` function
|
||||
|
||||
### Encryption Scheme
|
||||
|
||||
- **Algorithm**: AES-256-CTR (Counter mode)
|
||||
- **Key Size**: 256 bits (32 bytes)
|
||||
- **IV Generation**: Random 16-byte IV per object
|
||||
- **Storage Format**: `[IV][EncryptedData]` where IV is prepended to encrypted content
|
||||
|
||||
### Metadata Storage
|
||||
|
||||
SSE-C metadata is stored in the filer's extended attributes:
|
||||
```
|
||||
x-amz-server-side-encryption-customer-algorithm: "AES256"
|
||||
x-amz-server-side-encryption-customer-key-md5: "<md5-hash-of-key>"
|
||||
```
|
||||
|
||||
## API Compatibility
|
||||
|
||||
### Required Headers for Encryption (PUT/POST)
|
||||
```
|
||||
x-amz-server-side-encryption-customer-algorithm: AES256
|
||||
x-amz-server-side-encryption-customer-key: <base64-encoded-256-bit-key>
|
||||
x-amz-server-side-encryption-customer-key-md5: <md5-hash-of-key>
|
||||
```
|
||||
|
||||
### Required Headers for Decryption (GET/HEAD)
|
||||
Same headers as encryption - the server validates the key MD5 matches.
|
||||
|
||||
### Copy Operations
|
||||
Support for copy-source SSE-C headers:
|
||||
```
|
||||
x-amz-copy-source-server-side-encryption-customer-algorithm
|
||||
x-amz-copy-source-server-side-encryption-customer-key
|
||||
x-amz-copy-source-server-side-encryption-customer-key-md5
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
The implementation provides AWS-compatible error responses:
|
||||
|
||||
- **InvalidEncryptionAlgorithmError**: Non-AES256 algorithm specified
|
||||
- **InvalidArgument**: Invalid key format, size, or MD5 mismatch
|
||||
- **Missing customer key**: Object encrypted but no key provided
|
||||
- **Unnecessary customer key**: Object not encrypted but key provided
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Key Management**: Customer keys are never stored - only MD5 hashes for validation
|
||||
2. **IV Randomness**: Fresh random IV generated for each object
|
||||
3. **Transparent Security**: Volume servers never see unencrypted data
|
||||
4. **Key Validation**: Strict validation of key format, size, and MD5
|
||||
|
||||
## Testing
|
||||
|
||||
Comprehensive test suite covers:
|
||||
- Header validation and parsing (regular and copy-source)
|
||||
- Encryption/decryption round-trip
|
||||
- Error condition handling
|
||||
- Metadata extraction
|
||||
- Code reuse validation (DRY principle)
|
||||
- AWS S3 compatibility
|
||||
|
||||
Run tests with:
|
||||
```bash
|
||||
go test -v ./weed/s3api
|
||||
|
||||
## Usage Example
|
||||
|
||||
### Upload with SSE-C
|
||||
```bash
|
||||
# Generate a 256-bit key
|
||||
KEY=$(openssl rand -base64 32)
|
||||
KEY_MD5=$(echo -n "$KEY" | base64 -d | openssl dgst -md5 -binary | base64)
|
||||
|
||||
# Upload object with SSE-C
|
||||
curl -X PUT "http://localhost:8333/bucket/object" \
|
||||
-H "x-amz-server-side-encryption-customer-algorithm: AES256" \
|
||||
-H "x-amz-server-side-encryption-customer-key: $KEY" \
|
||||
-H "x-amz-server-side-encryption-customer-key-md5: $KEY_MD5" \
|
||||
--data-binary @file.txt
|
||||
```
|
||||
|
||||
### Download with SSE-C
|
||||
```bash
|
||||
# Download object with SSE-C (same key required)
|
||||
curl "http://localhost:8333/bucket/object" \
|
||||
-H "x-amz-server-side-encryption-customer-algorithm: AES256" \
|
||||
-H "x-amz-server-side-encryption-customer-key: $KEY" \
|
||||
-H "x-amz-server-side-encryption-customer-key-md5: $KEY_MD5"
|
||||
```
|
||||
|
||||
## Integration Points
|
||||
|
||||
### Existing SeaweedFS Features
|
||||
- **Filer Metadata**: Extends existing metadata storage
|
||||
- **Volume Servers**: No changes required - store encrypted data transparently
|
||||
- **S3 API**: Integrates seamlessly with existing handlers
|
||||
- **Versioning**: Compatible with object versioning
|
||||
- **Multipart Upload**: Ready for multipart upload integration
|
||||
|
||||
### Future Enhancements
|
||||
- **SSE-S3**: Server-managed encryption keys
|
||||
- **SSE-KMS**: External key management service integration
|
||||
- **Performance Optimization**: Hardware acceleration for encryption
|
||||
- **Compliance**: Enhanced audit logging for encrypted objects
|
||||
|
||||
## File Changes Summary
|
||||
|
||||
1. **`weed/s3api/s3_constants/header.go`** - Added SSE-C header constants
|
||||
2. **`weed/s3api/s3_sse_c.go`** - Core SSE-C implementation (NEW)
|
||||
3. **`weed/s3api/s3_sse_c_test.go`** - Comprehensive test suite (NEW)
|
||||
4. **`weed/s3api/s3err/s3api_errors.go`** - Added SSE-C error codes
|
||||
5. **`weed/s3api/s3api_object_handlers.go`** - GET/HEAD with SSE-C support
|
||||
6. **`weed/s3api/s3api_object_handlers_put.go`** - PUT with SSE-C support
|
||||
7. **`weed/server/filer_server_handlers_write_autochunk.go`** - Metadata storage
|
||||
|
||||
## Compliance
|
||||
|
||||
This implementation follows the [AWS S3 SSE-C specification](https://docs.aws.amazon.com/AmazonS3/latest/userguide/ServerSideEncryptionCustomerKeys.html) for maximum compatibility with existing S3 clients and tools.
|
||||
|
||||
## Performance Impact
|
||||
|
||||
- **Encryption Overhead**: Minimal CPU impact with efficient AES-CTR streaming
|
||||
- **Memory Usage**: Constant memory usage via streaming encryption/decryption
|
||||
- **Storage Overhead**: 16 bytes per object for IV storage
|
||||
- **Network**: No additional network overhead
|
||||
@@ -1,6 +1,8 @@
|
||||
module github.com/seaweedfs/seaweedfs
|
||||
|
||||
go 1.24.9
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.1
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.121.6 // indirect
|
||||
@@ -34,7 +36,7 @@ require (
|
||||
github.com/golang/snappy v1.0.0
|
||||
github.com/google/btree v1.1.3
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/google/wire v0.7.0 // indirect
|
||||
github.com/google/wire v0.6.0 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect
|
||||
@@ -91,27 +93,27 @@ require (
|
||||
go.etcd.io/etcd/client/v3 v3.6.6
|
||||
go.mongodb.org/mongo-driver v1.17.6
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
gocloud.dev v0.44.0
|
||||
gocloud.dev/pubsub/natspubsub v0.44.0
|
||||
gocloud.dev/pubsub/rabbitpubsub v0.44.0
|
||||
gocloud.dev v0.43.0
|
||||
gocloud.dev/pubsub/natspubsub v0.43.0
|
||||
gocloud.dev/pubsub/rabbitpubsub v0.43.0
|
||||
golang.org/x/crypto v0.46.0
|
||||
golang.org/x/exp v0.0.0-20250811191247-51f88131bc50
|
||||
golang.org/x/image v0.34.0
|
||||
golang.org/x/net v0.48.0
|
||||
golang.org/x/oauth2 v0.34.0
|
||||
golang.org/x/oauth2 v0.32.0 // indirect
|
||||
golang.org/x/sys v0.39.0
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
golang.org/x/tools v0.39.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
|
||||
google.golang.org/api v0.258.0
|
||||
google.golang.org/api v0.247.0
|
||||
google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79 // indirect
|
||||
google.golang.org/grpc v1.77.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
google.golang.org/protobuf v1.36.10
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
modernc.org/b v1.0.0 // indirect
|
||||
modernc.org/mathutil v1.7.1
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
modernc.org/sqlite v1.42.2
|
||||
modernc.org/sqlite v1.39.0
|
||||
modernc.org/strutil v1.2.1
|
||||
)
|
||||
|
||||
@@ -127,14 +129,15 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.0
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.6
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.6
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.93.0
|
||||
github.com/cognusion/imaging v1.0.2
|
||||
github.com/fluent/fluent-logger-golang v1.10.1
|
||||
github.com/getsentry/sentry-go v0.40.0
|
||||
github.com/getsentry/sentry-go v0.38.0
|
||||
github.com/gin-contrib/sessions v1.0.4
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||
github.com/google/flatbuffers/go v0.0.0-20230108230133-3b8644d32c50
|
||||
github.com/hanwen/go-fuse/v2 v2.9.0
|
||||
github.com/hashicorp/raft v1.7.3
|
||||
github.com/hashicorp/raft-boltdb/v2 v2.3.1
|
||||
github.com/hashicorp/vault/api v1.22.0
|
||||
@@ -144,21 +147,20 @@ require (
|
||||
github.com/mattn/go-sqlite3 v1.14.32
|
||||
github.com/minio/crc64nvme v1.1.1
|
||||
github.com/orcaman/concurrent-map/v2 v2.0.1
|
||||
github.com/parquet-go/parquet-go v0.26.3
|
||||
github.com/parquet-go/parquet-go v0.25.1
|
||||
github.com/pkg/sftp v1.13.10
|
||||
github.com/rabbitmq/amqp091-go v1.10.0
|
||||
github.com/rclone/rclone v1.71.2
|
||||
github.com/rdleal/intervalst v1.5.0
|
||||
github.com/redis/go-redis/v9 v9.17.2
|
||||
github.com/schollz/progressbar/v3 v3.19.0
|
||||
github.com/seaweedfs/go-fuse/v2 v2.9.1
|
||||
github.com/schollz/progressbar/v3 v3.18.0
|
||||
github.com/shirou/gopsutil/v4 v4.25.11
|
||||
github.com/tarantool/go-tarantool/v2 v2.4.1
|
||||
github.com/tikv/client-go/v2 v2.0.7
|
||||
github.com/xeipuuv/gojsonschema v1.2.0
|
||||
github.com/ydb-platform/ydb-go-sdk-auth-environ v0.5.1
|
||||
github.com/ydb-platform/ydb-go-sdk/v3 v3.125.1
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.7
|
||||
github.com/ydb-platform/ydb-go-sdk/v3 v3.122.0
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.6
|
||||
go.uber.org/atomic v1.11.0
|
||||
golang.org/x/sync v0.19.0
|
||||
golang.org/x/tools/godoc v0.1.0-deprecated
|
||||
@@ -169,7 +171,7 @@ require github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 // ind
|
||||
|
||||
require (
|
||||
cloud.google.com/go/longrunning v0.6.7 // indirect
|
||||
cloud.google.com/go/pubsub/v2 v2.2.1 // indirect
|
||||
cloud.google.com/go/pubsub/v2 v2.0.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 // indirect
|
||||
github.com/bazelbuild/rules_go v0.46.0 // indirect
|
||||
@@ -200,8 +202,6 @@ require (
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/lithammer/shortuuid/v3 v3.0.7 // indirect
|
||||
github.com/openzipkin/zipkin-go v0.4.3 // indirect
|
||||
github.com/parquet-go/bitpack v1.0.0 // indirect
|
||||
github.com/parquet-go/jsonlite v1.0.0 // indirect
|
||||
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect
|
||||
github.com/pierrre/geohash v1.0.0 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
@@ -227,7 +227,7 @@ require (
|
||||
|
||||
require (
|
||||
cel.dev/expr v0.24.0 // indirect
|
||||
cloud.google.com/go/auth v0.17.0 // indirect
|
||||
cloud.google.com/go/auth v0.16.5 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
cloud.google.com/go/iam v1.5.2 // indirect
|
||||
@@ -262,15 +262,15 @@ require (
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.18.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sns v1.34.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sqs v1.38.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 // indirect
|
||||
@@ -328,7 +328,7 @@ require (
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
||||
github.com/gorilla/context v1.1.2 // indirect
|
||||
github.com/gorilla/schema v1.4.1 // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
@@ -447,14 +447,14 @@ require (
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/arch v0.20.0 // indirect
|
||||
golang.org/x/term v0.38.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/validator.v2 v2.0.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.66.10 // indirect
|
||||
modernc.org/libc v1.66.3 // indirect
|
||||
moul.io/http2curl/v2 v2.3.0 // indirect
|
||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||
storj.io/common v0.0.0-20250808122759-804533d519c1 // indirect
|
||||
|
||||
@@ -86,8 +86,8 @@ cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVo
|
||||
cloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEarXQk6WEKkxYfL6kGIo=
|
||||
cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0=
|
||||
cloud.google.com/go/assuredworkloads v1.10.0/go.mod h1:kwdUQuXcedVdsIaKgKTp9t0UJkE5+PAVNhdQm4ZVq2E=
|
||||
cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4=
|
||||
cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ=
|
||||
cloud.google.com/go/auth v0.16.5 h1:mFWNQ2FEVWAliEQWpAdH80omXFokmrnbDhUS9cBywsI=
|
||||
cloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0=
|
||||
@@ -385,8 +385,8 @@ cloud.google.com/go/pubsub v1.27.1/go.mod h1:hQN39ymbV9geqBnfQq6Xf63yNhUAhv9CZhz
|
||||
cloud.google.com/go/pubsub v1.28.0/go.mod h1:vuXFpwaVoIPQMGXqRyUQigu/AX1S3IWugR9xznmcXX8=
|
||||
cloud.google.com/go/pubsub v1.50.1 h1:fzbXpPyJnSGvWXF1jabhQeXyxdbCIkXTpjXHy7xviBM=
|
||||
cloud.google.com/go/pubsub v1.50.1/go.mod h1:6YVJv3MzWJUVdvQXG081sFvS0dWQOdnV+oTo++q/xFk=
|
||||
cloud.google.com/go/pubsub/v2 v2.2.1 h1:3brZcshL3fIiD1qOxAE2QW9wxsfjioy014x4yC9XuYI=
|
||||
cloud.google.com/go/pubsub/v2 v2.2.1/go.mod h1:O5f0KHG9zDheZAd3z5rlCRhxt2JQtB+t/IYLKK3Bpvw=
|
||||
cloud.google.com/go/pubsub/v2 v2.0.0 h1:0qS6mRJ41gD1lNmM/vdm6bR7DQu6coQcVwD+VPf0Bz0=
|
||||
cloud.google.com/go/pubsub/v2 v2.0.0/go.mod h1:0aztFxNzVQIRSZ8vUr79uH2bS3jwLebwK6q1sgEub+E=
|
||||
cloud.google.com/go/pubsublite v1.5.0/go.mod h1:xapqNQ1CuLfGi23Yda/9l4bBCKz/wC3KIJ5gKcxveZg=
|
||||
cloud.google.com/go/pubsublite v1.6.0/go.mod h1:1eFCS0U11xlOuMFV/0iBqw3zP12kddMeCbj/F3FSj9k=
|
||||
cloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4=
|
||||
@@ -675,26 +675,26 @@ github.com/aws/aws-sdk-go-v2/credentials v1.19.6 h1:F9vWao2TwjV2MyiyVS+duza0NIRt
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.6/go.mod h1:SgHzKjEVsdQr6Opor0ihgWtkWdfRAIwxYzSJ8O85VHY=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 h1:80+uETIWS1BqjnN9uJ0dBUaETh+P1XwFy5vwHwK5r9k=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16/go.mod h1:wOOsYuxYuB/7FlnVtzeBYRcjSRtQpAW0hCP7tIULMwo=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.3 h1:4GNV1lhyELGjMz5ILMRxDvxvOaeo3Ux9Z69S1EgVMMQ=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.3/go.mod h1:br7KA6edAAqDGUYJ+zVVPAyMrPhnN+zdt17yTUT6FPw=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.18.4 h1:0SzCLoPRSK3qSydsaFQWugP+lOBCTPwfcBOm6222+UA=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.18.4/go.mod h1:JAet9FsBHjfdI+TnMBX4ModNNaQHAd3dc/Bk+cNsxeM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 h1:CjMzUs78RDDv4ROu3JnJn/Ig1r6ZD7/T2DXLLRpejic=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16/go.mod h1:uVW4OLBqbJXSHJYA9svT9BluSvvwbzLQ2Crf6UPzR3c=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.15 h1:NLYTEyZmVZo0Qh183sC8nC+ydJXOOeIL/qI/sS3PdLY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.15/go.mod h1:Z803iB3B0bc8oJV8zH2PERLRfQUJ2n2BXISpsA4+O1M=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 h1:DIBqIrJ7hv+e4CmIk2z3pyKT+3B6qVMgRsawHiR3qso=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7/go.mod h1:vLm00xmBke75UmpNvOcZQ/Q30ZFjbczeLFqGx5urmGo=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.6 h1:P1MU/SuhadGvg2jtviDXPEejU3jBNhoeeAlRadHzvHI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.6/go.mod h1:5KYaMG6wmVKMFBSfWoyG/zH8pWwzQFnKgpoSRlXHKdQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 h1:NSbvS17MlI2lurYgXnCOLvCFX38sBW4eiVER7+kkgsU=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16/go.mod h1:SwT8Tmqd4sA6G1qaGdzWCJN99bUmPGHfRwwq3G5Qb+A=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0 h1:MIWra+MSq53CFaXXAywB2qg9YvVZifkk6vEGl/1Qor0=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0/go.mod h1:79S2BdqCJpScXZA2y+cpZuocWsjGjJINyXnOsf5DTz8=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.15 h1:wsSQ4SVz5YE1crz0Ap7VBZrV4nNqZt4CIBBT8mnwoNc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.15/go.mod h1:I7sditnFGtYMIqPRU1QoHZAUrXkGp4SczmlLwrNPlD0=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.93.0 h1:IrbE3B8O9pm3lsg96AXIN5MXX4pECEuExh/A0Du3AuI=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.93.0/go.mod h1:/sJLzHtiiZvs6C1RbxS/anSAFwZD6oC6M/kotQzOiLw=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 h1:HpI7aMmJ+mm1wkSHIA2t5EaFFv5EFYXePW30p1EIrbQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4/go.mod h1:C5RdGMYGlfM0gYq/tifqgn4EbyX99V15P2V3R+VHbQU=
|
||||
github.com/aws/aws-sdk-go-v2/service/sns v1.34.7 h1:OBuZE9Wt8h2imuRktu+WfjiTGrnYdCIJg8IX92aalHE=
|
||||
@@ -927,8 +927,8 @@ github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBv
|
||||
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
||||
github.com/geoffgarside/ber v1.2.0 h1:/loowoRcs/MWLYmGX9QtIAbA+V/FrnVLsMMPhwiRm64=
|
||||
github.com/geoffgarside/ber v1.2.0/go.mod h1:jVPKeCbj6MvQZhwLYsGwaGI52oUorHoHKNecGT85ZCc=
|
||||
github.com/getsentry/sentry-go v0.40.0 h1:VTJMN9zbTvqDqPwheRVLcp0qcUcM+8eFivvGocAaSbo=
|
||||
github.com/getsentry/sentry-go v0.40.0/go.mod h1:eRXCoh3uvmjQLY6qu63BjUZnaBu5L5WhMV1RwYO8W5s=
|
||||
github.com/getsentry/sentry-go v0.38.0 h1:S8Xui7gLeAvXINVLMOaX94HnsDf1GexnfXGSNC4+KQs=
|
||||
github.com/getsentry/sentry-go v0.38.0/go.mod h1:eRXCoh3uvmjQLY6qu63BjUZnaBu5L5WhMV1RwYO8W5s=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gin-contrib/sessions v1.0.4 h1:ha6CNdpYiTOK/hTp05miJLbpTSNfOnFg5Jm2kbcqy8U=
|
||||
github.com/gin-contrib/sessions v1.0.4/go.mod h1:ccmkrb2z6iU2osiAHZG3x3J4suJK+OU27oqzlWOqQgs=
|
||||
@@ -1134,21 +1134,22 @@ github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4=
|
||||
github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18=
|
||||
github.com/google/wire v0.6.0 h1:HBkoIh4BdSxoyo9PveV8giw7ZsaBOvzWKfcg/6MrVwI=
|
||||
github.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
|
||||
@@ -1187,6 +1188,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+u
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
|
||||
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8=
|
||||
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
|
||||
github.com/hanwen/go-fuse/v2 v2.9.0 h1:0AOGUkHtbOVeyGLr0tXupiid1Vg7QB7M6YUcdmVdC58=
|
||||
github.com/hanwen/go-fuse/v2 v2.9.0/go.mod h1:yE6D2PqWwm3CbYRxFXV9xUd8Md5d6NG0WBs5spCswmI=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
@@ -1468,12 +1471,8 @@ github.com/orcaman/concurrent-map/v2 v2.0.1/go.mod h1:9Eq3TG2oBe5FirmYWQfYO5iH1q
|
||||
github.com/ory/dockertest/v3 v3.6.0/go.mod h1:4ZOpj8qBUmh8fcBSVzkH2bws2s91JdGvHUqan4GHEuQ=
|
||||
github.com/panjf2000/ants/v2 v2.11.3 h1:AfI0ngBoXJmYOpDh9m516vjqoUu2sLrIVgppI9TZVpg=
|
||||
github.com/panjf2000/ants/v2 v2.11.3/go.mod h1:8u92CYMUc6gyvTIw8Ru7Mt7+/ESnJahz5EVtqfrilek=
|
||||
github.com/parquet-go/bitpack v1.0.0 h1:AUqzlKzPPXf2bCdjfj4sTeacrUwsT7NlcYDMUQxPcQA=
|
||||
github.com/parquet-go/bitpack v1.0.0/go.mod h1:XnVk9TH+O40eOOmvpAVZ7K2ocQFrQwysLMnc6M/8lgs=
|
||||
github.com/parquet-go/jsonlite v1.0.0 h1:87QNdi56wOfsE5bdgas0vRzHPxfJgzrXGml1zZdd7VU=
|
||||
github.com/parquet-go/jsonlite v1.0.0/go.mod h1:nDjpkpL4EOtqs6NQugUsi0Rleq9sW/OtC1NnZEnxzF0=
|
||||
github.com/parquet-go/parquet-go v0.26.3 h1:kJY+xmjcR7BH77tyHqasJpIl3kch/6EIO3TW4tFj69M=
|
||||
github.com/parquet-go/parquet-go v0.26.3/go.mod h1:h9GcSt41Knf5qXI1tp1TfR8bDBUtvdUMzSKe26aZcHk=
|
||||
github.com/parquet-go/parquet-go v0.25.1 h1:l7jJwNM0xrk0cnIIptWMtnSnuxRkwq53S+Po3KG8Xgo=
|
||||
github.com/parquet-go/parquet-go v0.25.1/go.mod h1:AXBuotO1XiBtcqJb/FKFyjBG4aqa3aQAAWF3ZPzCanY=
|
||||
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
|
||||
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
@@ -1615,12 +1614,10 @@ github.com/samber/lo v1.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI=
|
||||
github.com/samber/lo v1.51.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
||||
github.com/sasha-s/go-deadlock v0.3.1 h1:sqv7fDNShgjcaxkO0JNcOAlr8B9+cV5Ey/OB71efZx0=
|
||||
github.com/sasha-s/go-deadlock v0.3.1/go.mod h1:F73l+cr82YSh10GxyRI6qZiCgK64VaZjwesgfQ1/iLM=
|
||||
github.com/schollz/progressbar/v3 v3.19.0 h1:Ea18xuIRQXLAUidVDox3AbwfUhD0/1IvohyTutOIFoc=
|
||||
github.com/schollz/progressbar/v3 v3.19.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec=
|
||||
github.com/schollz/progressbar/v3 v3.18.0 h1:uXdoHABRFmNIjUfte/Ex7WtuyVslrw2wVPQmCN62HpA=
|
||||
github.com/schollz/progressbar/v3 v3.18.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec=
|
||||
github.com/seaweedfs/cockroachdb-parser v0.0.0-20251021184156-909763b17138 h1:bX1vBF7GQjPeFQsCAZ8gCQGS/nJQnekL7gZ4Qg/pF4E=
|
||||
github.com/seaweedfs/cockroachdb-parser v0.0.0-20251021184156-909763b17138/go.mod h1:JSKCh6uCHBz91lQYFYHCyTrSVIPge4SUFVn28iwMNB0=
|
||||
github.com/seaweedfs/go-fuse/v2 v2.9.1 h1:gnKmfrKreCRGJmekGz5WMnNZqXEf9s9+V2hdWQdvx88=
|
||||
github.com/seaweedfs/go-fuse/v2 v2.9.1/go.mod h1:zABdmWEa6A0bwaBeEOBUeUkGIZlxUhcdv+V1Dcc/U/I=
|
||||
github.com/seaweedfs/goexif v1.0.3 h1:ve/OjI7dxPW8X9YQsv3JuVMaxEyF9Rvfd04ouL+Bz30=
|
||||
github.com/seaweedfs/goexif v1.0.3/go.mod h1:Oni780Z236sXpIQzk1XoJlTwqrJ02smEin9zQeff7Fk=
|
||||
github.com/seaweedfs/raft v1.1.6 h1:e83Xn0boscPnuSiBllUPeWRVS6JKrqJPYBozgFyBiC0=
|
||||
@@ -1786,8 +1783,8 @@ github.com/ydb-platform/ydb-go-sdk-auth-environ v0.5.1 h1:XaRxeVrOyl3y6v9CiYMWaF
|
||||
github.com/ydb-platform/ydb-go-sdk-auth-environ v0.5.1/go.mod h1:9YzkhlIymWaJGX6KMU3vh5sOf3UKbCXkG/ZdjaI3zNM=
|
||||
github.com/ydb-platform/ydb-go-sdk/v3 v3.44.0/go.mod h1:oSLwnuilwIpaF5bJJMAofnGgzPJusoI3zWMNb8I+GnM=
|
||||
github.com/ydb-platform/ydb-go-sdk/v3 v3.47.3/go.mod h1:bWnOIcUHd7+Sl7DN+yhyY1H/I61z53GczvwJgXMgvj0=
|
||||
github.com/ydb-platform/ydb-go-sdk/v3 v3.125.1 h1:YaqzRVbcncabB34YNjOl5ADomYUFva+6l74svIIIJUo=
|
||||
github.com/ydb-platform/ydb-go-sdk/v3 v3.125.1/go.mod h1:stS1mQYjbJvwwYaYzKyFY9eMiuVXWWXQA6T+SpOLg9c=
|
||||
github.com/ydb-platform/ydb-go-sdk/v3 v3.122.0 h1:uvqheUfoEJz0CLLLaYct69wpvoaosM/2joWFcbwoqfw=
|
||||
github.com/ydb-platform/ydb-go-sdk/v3 v3.122.0/go.mod h1:/LjMxb/rXmoGAAnImoqAFIlhO5ampHacbvDetQitCk4=
|
||||
github.com/ydb-platform/ydb-go-yc v0.12.1 h1:qw3Fa+T81+Kpu5Io2vYHJOwcrYrVjgJlT6t/0dOXJrA=
|
||||
github.com/ydb-platform/ydb-go-yc v0.12.1/go.mod h1:t/ZA4ECdgPWjAb4jyDe8AzQZB5dhpGbi3iCahFaNwBY=
|
||||
github.com/ydb-platform/ydb-go-yc-metadata v0.6.1 h1:9E5q8Nsy2RiJMZDNVy0A3KUrIMBPakJ2VgloeWbcI84=
|
||||
@@ -1822,8 +1819,8 @@ go.etcd.io/bbolt v1.4.2 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I=
|
||||
go.etcd.io/bbolt v1.4.2/go.mod h1:Is8rSHO/b4f3XigBC0lL0+4FwAQv3HXEEIgFMuKHceM=
|
||||
go.etcd.io/etcd/api/v3 v3.6.6 h1:mcaMp3+7JawWv69p6QShYWS8cIWUOl32bFLb6qf8pOQ=
|
||||
go.etcd.io/etcd/api/v3 v3.6.6/go.mod h1:f/om26iXl2wSkcTA1zGQv8reJRSLVdoEBsi4JdfMrx4=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.7 h1:vvzgyozz46q+TyeGBuFzVuI53/yd133CHceNb/AhBVs=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.7/go.mod h1:2IVulJ3FZ/czIGl9T4lMF1uxzrhRahLqe+hSgy+Kh7Q=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.6 h1:uoqgzSOv2H9KlIF5O1Lsd8sW+eMLuV6wzE3q5GJGQNs=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.6/go.mod h1:YngfUVmvsvOJ2rRgStIyHsKtOt9SZI2aBJrZiWJhCbI=
|
||||
go.etcd.io/etcd/client/v3 v3.6.6 h1:G5z1wMf5B9SNexoxOHUGBaULurOZPIgGPsW6CN492ec=
|
||||
go.etcd.io/etcd/client/v3 v3.6.6/go.mod h1:36Qv6baQ07znPR3+n7t+Rk5VHEzVYPvFfGmfF4wBHV8=
|
||||
go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss=
|
||||
@@ -1891,12 +1888,12 @@ go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
gocloud.dev v0.44.0 h1:iVyMAqFl2r6xUy7M4mfqwlN+21UpJoEtgHEcfiLMUXs=
|
||||
gocloud.dev v0.44.0/go.mod h1:ZmjROXGdC/eKZLF1N+RujDlFRx3D+4Av2thREKDMVxY=
|
||||
gocloud.dev/pubsub/natspubsub v0.44.0 h1:1Us76ckkdgtiE1p1rJZ+38b9TQP051bmjAiQlFQzYrM=
|
||||
gocloud.dev/pubsub/natspubsub v0.44.0/go.mod h1:PvVAGIhL14PWGwWIXX/zAK42ixr2/PKP4Q4yMiAUraQ=
|
||||
gocloud.dev/pubsub/rabbitpubsub v0.44.0 h1:MpRIO6XJ/JTqrlUWt3CxwDe1LvaiXUVu4sS5cv4f/AM=
|
||||
gocloud.dev/pubsub/rabbitpubsub v0.44.0/go.mod h1:BB9+qT3r6g4M5+4asiXaEeqw4QAOzsWusO5krYaqkdA=
|
||||
gocloud.dev v0.43.0 h1:aW3eq4RMyehbJ54PMsh4hsp7iX8cO/98ZRzJJOzN/5M=
|
||||
gocloud.dev v0.43.0/go.mod h1:eD8rkg7LhKUHrzkEdLTZ+Ty/vgPHPCd+yMQdfelQVu4=
|
||||
gocloud.dev/pubsub/natspubsub v0.43.0 h1:k35tFoaorvD9Fa26zVEEzyXiMOEyXNHc0pBOmRYvQI0=
|
||||
gocloud.dev/pubsub/natspubsub v0.43.0/go.mod h1:xJn8TO8pGYieDn6AsRFsYfhQW8cnC+xGmG9APGNxkpQ=
|
||||
gocloud.dev/pubsub/rabbitpubsub v0.43.0 h1:6nNZFSlJ1dk2GujL8PFltfLz3vC6IbrpjGS4FTduo1s=
|
||||
gocloud.dev/pubsub/rabbitpubsub v0.43.0/go.mod h1:sEaueAGat+OASRoB3QDkghCtibKttgg7X6zsPTm1pl0=
|
||||
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
|
||||
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
@@ -1916,6 +1913,7 @@ golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
@@ -1986,6 +1984,7 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||
@@ -2055,6 +2054,7 @@ golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
@@ -2088,8 +2088,8 @@ golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri
|
||||
golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec=
|
||||
golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
|
||||
golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
|
||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
|
||||
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -2214,6 +2214,7 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
@@ -2232,6 +2233,7 @@ golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
@@ -2267,8 +2269,8 @@ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxb
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -2339,6 +2341,7 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
|
||||
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||
@@ -2419,8 +2422,8 @@ google.golang.org/api v0.106.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/
|
||||
google.golang.org/api v0.107.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY=
|
||||
google.golang.org/api v0.108.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY=
|
||||
google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI=
|
||||
google.golang.org/api v0.258.0 h1:IKo1j5FBlN74fe5isA2PVozN3Y5pwNKriEgAXPOkDAc=
|
||||
google.golang.org/api v0.258.0/go.mod h1:qhOMTQEZ6lUps63ZNq9jhODswwjkjYYguA7fA3TBFww=
|
||||
google.golang.org/api v0.247.0 h1:tSd/e0QrUlLsrwMKmkbQhYVa109qIintOls2Wh6bngc=
|
||||
google.golang.org/api v0.247.0/go.mod h1:r1qZOPmxXffXg6xS5uhx16Fa/UFY8QU/K4bfKrnvovM=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
@@ -2558,8 +2561,8 @@ google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79 h1:Nt6z9UHqSlIdIGJ
|
||||
google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79/go.mod h1:kTmlBHMPqR5uCZPBvwa2B18mvubkjyY3CRLI0c6fj0s=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
@@ -2623,8 +2626,8 @@ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -2675,19 +2678,19 @@ modernc.org/b v1.0.0/go.mod h1:uZWcZfRj1BpYzfN9JTerzlNUnnPsV9O2ZA8JsRcubNg=
|
||||
modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
|
||||
modernc.org/cc/v3 v3.36.2/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
|
||||
modernc.org/cc/v3 v3.36.3/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
|
||||
modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4=
|
||||
modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM=
|
||||
modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc=
|
||||
modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw=
|
||||
modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ=
|
||||
modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ=
|
||||
modernc.org/ccgo/v3 v3.16.8/go.mod h1:zNjwkizS+fIFDrDjIAgBSCLkWbJuHF+ar3QRn+Z9aws=
|
||||
modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo=
|
||||
modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=
|
||||
modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q=
|
||||
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
|
||||
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
|
||||
modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
|
||||
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
|
||||
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM=
|
||||
modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
@@ -2700,8 +2703,8 @@ modernc.org/libc v1.16.17/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU=
|
||||
modernc.org/libc v1.16.19/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA=
|
||||
modernc.org/libc v1.17.0/go.mod h1:XsgLldpP4aWlPlsjqKRdHPqCxCjISdHfM/yeWC5GyW0=
|
||||
modernc.org/libc v1.17.1/go.mod h1:FZ23b+8LjxZs7XtFMbSzL/EhPxNbfZbErxEHc7cbD9s=
|
||||
modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A=
|
||||
modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I=
|
||||
modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ=
|
||||
modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8=
|
||||
modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
@@ -2720,8 +2723,8 @@ modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.18.1/go.mod h1:6ho+Gow7oX5V+OiOQ6Tr4xeqbx13UZ6t+Fw9IRUG4d4=
|
||||
modernc.org/sqlite v1.42.2 h1:7hkZUNJvJFN2PgfUdjni9Kbvd4ef4mNLOu0B9FGxM74=
|
||||
modernc.org/sqlite v1.42.2/go.mod h1:+VkC6v3pLOAE0A0uVucQEcbVW0I5nHCeDaBf+DpsQT8=
|
||||
modernc.org/sqlite v1.39.0 h1:6bwu9Ooim0yVYA7IZn9demiQk/Ejp0BtTjBWFLymSeY=
|
||||
modernc.org/sqlite v1.39.0/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
|
||||
modernc.org/strutil v1.1.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs=
|
||||
modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw=
|
||||
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
apiVersion: v1
|
||||
description: SeaweedFS
|
||||
name: seaweedfs
|
||||
appVersion: "4.06"
|
||||
appVersion: "4.03"
|
||||
# Dev note: Trigger a helm chart release by `git tag -a helm-<version>`
|
||||
version: 4.0.406
|
||||
version: 4.0.403
|
||||
|
||||
@@ -25,6 +25,12 @@ spec:
|
||||
port: {{ .Values.admin.grpcPort }}
|
||||
targetPort: {{ .Values.admin.grpcPort }}
|
||||
protocol: TCP
|
||||
{{- if .Values.admin.metricsPort }}
|
||||
- name: "metrics"
|
||||
port: {{ .Values.admin.metricsPort }}
|
||||
targetPort: {{ .Values.admin.metricsPort }}
|
||||
protocol: TCP
|
||||
{{- end }}
|
||||
selector:
|
||||
app.kubernetes.io/name: {{ template "seaweedfs.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{{- if .Values.admin.enabled }}
|
||||
{{- if .Values.admin.metricsPort }}
|
||||
{{- if .Values.global.monitoring.enabled }}
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
@@ -21,7 +22,7 @@ metadata:
|
||||
spec:
|
||||
endpoints:
|
||||
- interval: 30s
|
||||
port: http
|
||||
port: metrics
|
||||
scrapeTimeout: 5s
|
||||
selector:
|
||||
matchLabels:
|
||||
@@ -29,3 +30,4 @@ spec:
|
||||
app.kubernetes.io/component: admin
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
@@ -213,6 +213,10 @@ spec:
|
||||
name: http
|
||||
- containerPort: {{ .Values.admin.grpcPort }}
|
||||
name: grpc
|
||||
{{- if .Values.admin.metricsPort }}
|
||||
- containerPort: {{ .Values.admin.metricsPort }}
|
||||
name: metrics
|
||||
{{- end }}
|
||||
{{- if .Values.admin.readinessProbe.enabled }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
|
||||
@@ -229,7 +229,7 @@ spec:
|
||||
- name: seaweedfs-filer-log-volume
|
||||
mountPath: "/logs/"
|
||||
{{- end }}
|
||||
{{- if and .Values.filer.s3.enabled .Values.filer.s3.enableAuth }}
|
||||
{{- if .Values.filer.s3.enableAuth }}
|
||||
- name: config-users
|
||||
mountPath: /etc/sw
|
||||
readOnly: true
|
||||
@@ -263,7 +263,7 @@ spec:
|
||||
mountPath: /usr/local/share/ca-certificates/filer/
|
||||
- name: client-cert
|
||||
readOnly: true
|
||||
mountPath: /usr/local/share/ca-certificates/client
|
||||
mountPath: /usr/local/share/ca-certificates/client/
|
||||
{{- end }}
|
||||
{{ tpl .Values.filer.extraVolumeMounts . | nindent 12 | trim }}
|
||||
ports:
|
||||
@@ -281,18 +281,12 @@ spec:
|
||||
name: swfs-s3-tls
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- $isJwtEnabled := or .Values.global.securityConfig.jwtSigning.filerWrite .Values.global.securityConfig.jwtSigning.filerRead }}
|
||||
{{- if .Values.filer.readinessProbe.enabled }}
|
||||
readinessProbe:
|
||||
{{- if or $isJwtEnabled .Values.filer.readinessProbe.tcpSocket }}
|
||||
tcpSocket:
|
||||
port: {{ if $isJwtEnabled }}{{ .Values.filer.port }}{{ else }}{{ .Values.filer.readinessProbe.tcpSocket.port }}{{ end }}
|
||||
{{- else }}
|
||||
httpGet:
|
||||
path: {{ .Values.filer.readinessProbe.httpGet.path }}
|
||||
port: {{ .Values.filer.port }}
|
||||
scheme: {{ .Values.filer.readinessProbe.httpGet.scheme }}
|
||||
{{- end }}
|
||||
initialDelaySeconds: {{ .Values.filer.readinessProbe.initialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.filer.readinessProbe.periodSeconds }}
|
||||
successThreshold: {{ .Values.filer.readinessProbe.successThreshold }}
|
||||
@@ -301,15 +295,10 @@ spec:
|
||||
{{- end }}
|
||||
{{- if .Values.filer.livenessProbe.enabled }}
|
||||
livenessProbe:
|
||||
{{- if or $isJwtEnabled .Values.filer.livenessProbe.tcpSocket }}
|
||||
tcpSocket:
|
||||
port: {{ if $isJwtEnabled }}{{ .Values.filer.port }}{{ else }}{{ .Values.filer.livenessProbe.tcpSocket.port }}{{ end }}
|
||||
{{- else }}
|
||||
httpGet:
|
||||
path: {{ .Values.filer.livenessProbe.httpGet.path }}
|
||||
port: {{ .Values.filer.port }}
|
||||
scheme: {{ .Values.filer.livenessProbe.httpGet.scheme }}
|
||||
{{- end }}
|
||||
initialDelaySeconds: {{ .Values.filer.livenessProbe.initialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.filer.livenessProbe.periodSeconds }}
|
||||
successThreshold: {{ .Values.filer.livenessProbe.successThreshold }}
|
||||
|
||||
@@ -4,13 +4,6 @@
|
||||
{{- /* Determine service name based on deployment mode */}}
|
||||
{{- $serviceName := ternary (printf "%s-all-in-one" (include "seaweedfs.name" .)) (printf "%s-s3" (include "seaweedfs.name" .)) .Values.allInOne.enabled }}
|
||||
{{- $s3Port := .Values.allInOne.s3.port | default .Values.s3.port }}
|
||||
{{- /* Build hosts list - support both legacy .host (string) and new .hosts (array) for backwards compatibility */}}
|
||||
{{- $hosts := list }}
|
||||
{{- if kindIs "slice" .Values.s3.ingress.host }}
|
||||
{{- $hosts = .Values.s3.ingress.host }}
|
||||
{{- else if .Values.s3.ingress.host }}
|
||||
{{- $hosts = list .Values.s3.ingress.host }}
|
||||
{{- end }}
|
||||
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion }}
|
||||
@@ -37,25 +30,6 @@ spec:
|
||||
tls:
|
||||
{{ .Values.s3.ingress.tls | default list | toYaml | nindent 6}}
|
||||
rules:
|
||||
{{- if $hosts }}
|
||||
{{- range $hosts }}
|
||||
- host: {{ . | quote }}
|
||||
http:
|
||||
paths:
|
||||
- path: {{ $.Values.s3.ingress.path | quote }}
|
||||
pathType: {{ $.Values.s3.ingress.pathType | quote }}
|
||||
backend:
|
||||
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
service:
|
||||
name: {{ $serviceName }}
|
||||
port:
|
||||
number: {{ $s3Port }}
|
||||
{{- else }}
|
||||
serviceName: {{ $serviceName }}
|
||||
servicePort: {{ $s3Port }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- else }}
|
||||
- http:
|
||||
paths:
|
||||
- path: {{ .Values.s3.ingress.path | quote }}
|
||||
@@ -70,5 +44,7 @@ spec:
|
||||
serviceName: {{ $serviceName }}
|
||||
servicePort: {{ $s3Port }}
|
||||
{{- end }}
|
||||
{{- if .Values.s3.ingress.host }}
|
||||
host: {{ .Values.s3.ingress.host | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
@@ -176,9 +176,6 @@ spec:
|
||||
{{- if $volume.dataCenter }}
|
||||
-dataCenter={{ $volume.dataCenter }} \
|
||||
{{- end }}
|
||||
{{- if $volume.id }}
|
||||
-id={{ $volume.id }} \
|
||||
{{- end }}
|
||||
-ip.bind={{ $volume.ipBind }} \
|
||||
-readMode={{ $volume.readMode }} \
|
||||
{{- if $volume.whiteList }}
|
||||
|
||||
@@ -134,14 +134,11 @@ spec:
|
||||
{{- if .Values.worker.adminServer }}
|
||||
-admin={{ .Values.worker.adminServer }} \
|
||||
{{- else }}
|
||||
-admin={{ template "seaweedfs.name" . }}-admin.{{ .Release.Namespace }}:{{ .Values.admin.port }}{{ if .Values.admin.grpcPort }}.{{ .Values.admin.grpcPort }}{{ end }} \
|
||||
-admin={{ template "seaweedfs.name" . }}-admin.{{ .Release.Namespace }}:{{ .Values.admin.grpcPort }} \
|
||||
{{- end }}
|
||||
-capabilities={{ .Values.worker.capabilities }} \
|
||||
-maxConcurrent={{ .Values.worker.maxConcurrent }} \
|
||||
-workingDir={{ .Values.worker.workingDir }}{{- if or .Values.worker.metricsPort .Values.worker.extraArgs }} \{{ end }}
|
||||
{{- if .Values.worker.metricsPort }}
|
||||
-metricsPort={{ .Values.worker.metricsPort }}{{- if .Values.worker.extraArgs }} \{{ end }}
|
||||
{{- end }}
|
||||
-workingDir={{ .Values.worker.workingDir }}{{- if .Values.worker.extraArgs }} \{{ end }}
|
||||
{{- range $index, $arg := .Values.worker.extraArgs }}
|
||||
{{ $arg }}{{- if lt $index (sub (len $.Values.worker.extraArgs) 1) }} \{{ end }}
|
||||
{{- end }}
|
||||
@@ -190,13 +187,9 @@ spec:
|
||||
{{- end }}
|
||||
{{- if .Values.worker.livenessProbe.enabled }}
|
||||
livenessProbe:
|
||||
{{- if .Values.worker.livenessProbe.httpGet }}
|
||||
httpGet:
|
||||
path: {{ .Values.worker.livenessProbe.httpGet.path }}
|
||||
port: {{ .Values.worker.livenessProbe.httpGet.port }}
|
||||
{{- else if .Values.worker.livenessProbe.tcpSocket }}
|
||||
{{- with .Values.worker.livenessProbe.tcpSocket }}
|
||||
tcpSocket:
|
||||
port: {{ .Values.worker.livenessProbe.tcpSocket.port }}
|
||||
port: {{ .port }}
|
||||
{{- end }}
|
||||
initialDelaySeconds: {{ .Values.worker.livenessProbe.initialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.worker.livenessProbe.periodSeconds }}
|
||||
@@ -206,13 +199,9 @@ spec:
|
||||
{{- end }}
|
||||
{{- if .Values.worker.readinessProbe.enabled }}
|
||||
readinessProbe:
|
||||
{{- if .Values.worker.readinessProbe.httpGet }}
|
||||
httpGet:
|
||||
path: {{ .Values.worker.readinessProbe.httpGet.path }}
|
||||
port: {{ .Values.worker.readinessProbe.httpGet.port }}
|
||||
{{- else if .Values.worker.readinessProbe.tcpSocket }}
|
||||
{{- with .Values.worker.readinessProbe.tcpSocket }}
|
||||
tcpSocket:
|
||||
port: {{ .Values.worker.readinessProbe.tcpSocket.port }}
|
||||
port: {{ .port }}
|
||||
{{- end }}
|
||||
initialDelaySeconds: {{ .Values.worker.readinessProbe.initialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.worker.readinessProbe.periodSeconds }}
|
||||
|
||||
@@ -261,10 +261,10 @@ master:
|
||||
tls: []
|
||||
|
||||
extraEnvironmentVars:
|
||||
WEED_MASTER_VOLUME_GROWTH_COPY_1: "7"
|
||||
WEED_MASTER_VOLUME_GROWTH_COPY_2: "6"
|
||||
WEED_MASTER_VOLUME_GROWTH_COPY_3: "3"
|
||||
WEED_MASTER_VOLUME_GROWTH_COPY_OTHER: "1"
|
||||
WEED_MASTER_VOLUME_GROWTH_COPY_1: '7'
|
||||
WEED_MASTER_VOLUME_GROWTH_COPY_2: '6'
|
||||
WEED_MASTER_VOLUME_GROWTH_COPY_3: '3'
|
||||
WEED_MASTER_VOLUME_GROWTH_COPY_OTHER: '1'
|
||||
|
||||
# used to configure livenessProbe on master-server containers
|
||||
#
|
||||
@@ -348,16 +348,16 @@ volume:
|
||||
# maxVolumes: 0 # If set to zero on non-windows OS, the limit will be auto configured. (default "7")
|
||||
|
||||
dataDirs:
|
||||
- name: data1
|
||||
type: "hostPath"
|
||||
hostPathPrefix: /ssd
|
||||
maxVolumes: 0
|
||||
- name: data1
|
||||
type: "hostPath"
|
||||
hostPathPrefix: /ssd
|
||||
maxVolumes: 0
|
||||
|
||||
# - name: data2
|
||||
# type: "persistentVolumeClaim"
|
||||
# storageClass: "yourClassNameOfChoice"
|
||||
# size: "800Gi"
|
||||
# maxVolumes: 0
|
||||
# - name: data2
|
||||
# type: "persistentVolumeClaim"
|
||||
# storageClass: "yourClassNameOfChoice"
|
||||
# size: "800Gi"
|
||||
# maxVolumes: 0
|
||||
|
||||
# This will automatically create a job for patching Kubernetes resources if the dataDirs type is 'persistentVolumeClaim' and the size has changed.
|
||||
resizeHook:
|
||||
@@ -397,13 +397,10 @@ volume:
|
||||
# limit background compaction or copying speed in mega bytes per second
|
||||
compactionMBps: "50"
|
||||
|
||||
|
||||
# Volume server's rack name
|
||||
rack: null
|
||||
|
||||
# Stable identifier for the volume server, independent of IP address
|
||||
# Useful for Kubernetes environments with hostPath volumes to maintain stable identity
|
||||
id: null
|
||||
|
||||
# Volume server's data center name
|
||||
dataCenter: null
|
||||
|
||||
@@ -845,14 +842,14 @@ filer:
|
||||
|
||||
# secret env variables
|
||||
secretExtraEnvironmentVars: {}
|
||||
# WEED_POSTGRES_USERNAME:
|
||||
# secretKeyRef:
|
||||
# name: postgres-credentials
|
||||
# key: username
|
||||
# WEED_POSTGRES_PASSWORD:
|
||||
# secretKeyRef:
|
||||
# name: postgres-credentials
|
||||
# key: password
|
||||
# WEED_POSTGRES_USERNAME:
|
||||
# secretKeyRef:
|
||||
# name: postgres-credentials
|
||||
# key: username
|
||||
# WEED_POSTGRES_PASSWORD:
|
||||
# secretKeyRef:
|
||||
# name: postgres-credentials
|
||||
# key: password
|
||||
|
||||
s3:
|
||||
enabled: false
|
||||
@@ -1011,7 +1008,7 @@ s3:
|
||||
ingress:
|
||||
enabled: false
|
||||
className: ""
|
||||
# host: false for "*" hostname, or an array for multiple hostnames
|
||||
# host: false for "*" hostname
|
||||
host: "seaweedfs.cluster.local"
|
||||
path: "/"
|
||||
pathType: Prefix
|
||||
@@ -1098,6 +1095,7 @@ admin:
|
||||
replicas: 1
|
||||
port: 23646 # Default admin port
|
||||
grpcPort: 33646 # Default gRPC port for worker connections
|
||||
metricsPort: 9327
|
||||
loggingOverrideLevel: null
|
||||
|
||||
# Admin authentication
|
||||
@@ -1229,6 +1227,7 @@ worker:
|
||||
metricsPort: 9327
|
||||
|
||||
# Admin server to connect to
|
||||
# Format: "host:port" or auto-discover from admin service
|
||||
adminServer: ""
|
||||
|
||||
# Worker capabilities - comma-separated list
|
||||
@@ -1303,11 +1302,10 @@ worker:
|
||||
extraEnvironmentVars: {}
|
||||
|
||||
# Health checks for worker pods
|
||||
# Workers expose /health (liveness) and /ready (readiness) endpoints on the metricsPort
|
||||
# Since workers do not have an HTTP endpoint, a tcpSocket probe on the metrics port is recommended.
|
||||
livenessProbe:
|
||||
enabled: true
|
||||
httpGet:
|
||||
path: /health
|
||||
tcpSocket:
|
||||
port: metrics
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 60
|
||||
@@ -1317,8 +1315,7 @@ worker:
|
||||
|
||||
readinessProbe:
|
||||
enabled: true
|
||||
httpGet:
|
||||
path: /ready
|
||||
tcpSocket:
|
||||
port: metrics
|
||||
initialDelaySeconds: 20
|
||||
periodSeconds: 15
|
||||
|
||||
@@ -87,7 +87,7 @@ public class FilerGrpcClient {
|
||||
.maxHeaderListSize(16 * 1024 * 1024)
|
||||
.keepAliveTime(KEEP_ALIVE_TIME_SECONDS, TimeUnit.SECONDS)
|
||||
.keepAliveTimeout(KEEP_ALIVE_TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||
.keepAliveWithoutCalls(false)
|
||||
.keepAliveWithoutCalls(true)
|
||||
.withOption(io.grpc.netty.shaded.io.netty.channel.ChannelOption.SO_RCVBUF, 16 * 1024 * 1024)
|
||||
.withOption(io.grpc.netty.shaded.io.netty.channel.ChannelOption.SO_SNDBUF, 16 * 1024 * 1024);
|
||||
|
||||
|
||||
@@ -3,14 +3,13 @@ package main
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/seaweedfs/seaweedfs/other/mq_client_example/example"
|
||||
"github.com/seaweedfs/seaweedfs/weed/mq/client/agent_client"
|
||||
"github.com/seaweedfs/seaweedfs/weed/mq/schema"
|
||||
"log"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/other/mq_client_example/example"
|
||||
"github.com/seaweedfs/seaweedfs/weed/mq/client/agent_client"
|
||||
"github.com/seaweedfs/seaweedfs/weed/mq/schema"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -3,14 +3,13 @@ package main
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/other/mq_client_example/example"
|
||||
"github.com/seaweedfs/seaweedfs/weed/mq/client/agent_client"
|
||||
"github.com/seaweedfs/seaweedfs/weed/mq/topic"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/schema_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module seaweedfs-rdma-sidecar
|
||||
|
||||
go 1.24.9
|
||||
go 1.24
|
||||
|
||||
require (
|
||||
github.com/seaweedfs/seaweedfs v0.0.0-00010101000000-000000000000
|
||||
@@ -18,27 +18,27 @@ require (
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||
github.com/prometheus/client_golang v1.23.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.66.1 // indirect
|
||||
github.com/prometheus/procfs v0.19.2 // indirect
|
||||
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||
github.com/prometheus/common v0.65.0 // indirect
|
||||
github.com/prometheus/procfs v0.17.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.7.0 // indirect
|
||||
github.com/seaweedfs/goexif v1.0.3 // indirect
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/spf13/viper v1.21.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.12.0 // indirect
|
||||
github.com/spf13/cast v1.7.1 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/spf13/viper v1.20.1 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/image v0.34.0 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
|
||||
google.golang.org/grpc v1.77.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
golang.org/x/image v0.30.0 // indirect
|
||||
golang.org/x/net v0.43.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 // indirect
|
||||
google.golang.org/grpc v1.74.2 // indirect
|
||||
google.golang.org/protobuf v1.36.7 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
|
||||
@@ -44,43 +44,34 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
|
||||
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
|
||||
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
||||
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
|
||||
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
|
||||
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
|
||||
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
|
||||
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
||||
github.com/seaweedfs/goexif v1.0.3 h1:ve/OjI7dxPW8X9YQsv3JuVMaxEyF9Rvfd04ouL+Bz30=
|
||||
github.com/seaweedfs/goexif v1.0.3/go.mod h1:Oni780Z236sXpIQzk1XoJlTwqrJ02smEin9zQeff7Fk=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
||||
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
|
||||
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
||||
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
|
||||
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
|
||||
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
@@ -109,26 +100,19 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
golang.org/x/image v0.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4=
|
||||
golang.org/x/image v0.30.0/go.mod h1:SAEUTxCCMWSrJcCy/4HwavEsfZZJlYxeHLc6tTiAe/c=
|
||||
golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 h1:MAKi5q709QWfnkkpNQ0M12hYJ1+e8qYVDyowc4U1XZM=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
|
||||
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
|
||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
|
||||
google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
|
||||
@@ -419,7 +419,7 @@ func (c *Client) Read(ctx context.Context, req *ReadRequest) (*ReadResponse, err
|
||||
}).Info("✅ RDMA read completed successfully")
|
||||
|
||||
// MOCK DATA IMPLEMENTATION - FOR DEVELOPMENT/TESTING ONLY
|
||||
//
|
||||
//
|
||||
// This section generates placeholder data for the mock RDMA implementation.
|
||||
// In a production RDMA implementation, this should be replaced with:
|
||||
//
|
||||
@@ -472,7 +472,7 @@ func (c *Client) ReadFileRange(ctx context.Context, fileID string, offset, size
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid file ID %s: %w", fileID, err)
|
||||
}
|
||||
|
||||
|
||||
req := &ReadRequest{
|
||||
VolumeID: volumeID,
|
||||
NeedleID: needleID,
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"seaweedfs-rdma-sidecar/pkg/rdma"
|
||||
|
||||
@@ -7,11 +7,10 @@
|
||||
package proto
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -7,7 +7,7 @@ toolchain go1.25.0
|
||||
require (
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/seaweedfs/seaweedfs v0.0.0-00010101000000-000000000000
|
||||
google.golang.org/protobuf v1.36.11
|
||||
google.golang.org/protobuf v1.36.8
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -16,9 +16,9 @@ require (
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.66.1 // indirect
|
||||
github.com/prometheus/procfs v0.19.2 // indirect
|
||||
github.com/prometheus/procfs v0.17.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
)
|
||||
|
||||
replace github.com/seaweedfs/seaweedfs => ../..
|
||||
|
||||
@@ -26,7 +26,6 @@ github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9Z
|
||||
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
||||
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
|
||||
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
|
||||
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
@@ -37,10 +36,8 @@ go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
|
||||
@@ -18,8 +18,6 @@ import (
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/shell"
|
||||
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
|
||||
"github.com/seaweedfs/seaweedfs/weed/storage/types"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
"github.com/seaweedfs/seaweedfs/weed/wdclient"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -35,8 +33,9 @@ func TestECEncodingVolumeLocationTimingBug(t *testing.T) {
|
||||
}
|
||||
|
||||
// Create temporary directory for test data
|
||||
// Using t.TempDir() automatically preserves logs when tests fail
|
||||
testDir := t.TempDir()
|
||||
testDir, err := os.MkdirTemp("", "seaweedfs_ec_integration_test_")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(testDir)
|
||||
|
||||
// Start SeaweedFS cluster with multiple volume servers
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
@@ -245,8 +244,9 @@ func TestECEncodingMasterTimingRaceCondition(t *testing.T) {
|
||||
}
|
||||
|
||||
// Create temporary directory for test data
|
||||
// Using t.TempDir() automatically preserves logs when tests fail
|
||||
testDir := t.TempDir()
|
||||
testDir, err := os.MkdirTemp("", "seaweedfs_ec_race_test_")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(testDir)
|
||||
|
||||
// Start SeaweedFS cluster
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
@@ -783,8 +783,9 @@ func TestDiskAwareECRebalancing(t *testing.T) {
|
||||
t.Skip("Skipping disk-aware integration test in short mode")
|
||||
}
|
||||
|
||||
// Using t.TempDir() automatically preserves logs when tests fail
|
||||
testDir := t.TempDir()
|
||||
testDir, err := os.MkdirTemp("", "seaweedfs_disk_aware_ec_test_")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(testDir)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 180*time.Second)
|
||||
defer cancel()
|
||||
@@ -1216,8 +1217,9 @@ func TestECDiskTypeSupport(t *testing.T) {
|
||||
t.Skip("Skipping disk type integration test in short mode")
|
||||
}
|
||||
|
||||
// Using t.TempDir() automatically preserves logs when tests fail
|
||||
testDir := t.TempDir()
|
||||
testDir, err := os.MkdirTemp("", "seaweedfs_ec_disktype_test_")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(testDir)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 180*time.Second)
|
||||
defer cancel()
|
||||
@@ -1556,8 +1558,9 @@ func TestECDiskTypeMixedCluster(t *testing.T) {
|
||||
t.Skip("Skipping mixed disk type integration test in short mode")
|
||||
}
|
||||
|
||||
// Using t.TempDir() automatically preserves logs when tests fail
|
||||
testDir := t.TempDir()
|
||||
testDir, err := os.MkdirTemp("", "seaweedfs_ec_mixed_disktype_test_")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(testDir)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 180*time.Second)
|
||||
defer cancel()
|
||||
@@ -1745,8 +1748,9 @@ func TestEvacuationFallbackBehavior(t *testing.T) {
|
||||
t.Skip("Skipping evacuation fallback test in short mode")
|
||||
}
|
||||
|
||||
// Using t.TempDir() automatically preserves logs when tests fail
|
||||
testDir := t.TempDir()
|
||||
testDir, err := os.MkdirTemp("", "seaweedfs_evacuation_fallback_test_")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(testDir)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 180*time.Second)
|
||||
defer cancel()
|
||||
@@ -1838,8 +1842,9 @@ func TestCrossRackECPlacement(t *testing.T) {
|
||||
t.Skip("Skipping cross-rack EC placement test in short mode")
|
||||
}
|
||||
|
||||
// Using t.TempDir() automatically preserves logs when tests fail
|
||||
testDir := t.TempDir()
|
||||
testDir, err := os.MkdirTemp("", "seaweedfs_cross_rack_ec_test_")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(testDir)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 180*time.Second)
|
||||
defer cancel()
|
||||
@@ -2191,8 +2196,9 @@ func TestECEncodeReplicatedVolumeSync(t *testing.T) {
|
||||
}
|
||||
|
||||
// Create temporary directory for test data
|
||||
// Using t.TempDir() automatically preserves logs when tests fail
|
||||
testDir := t.TempDir()
|
||||
testDir, err := os.MkdirTemp("", "seaweedfs_ec_replica_sync_test_")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(testDir)
|
||||
|
||||
// Start SeaweedFS cluster
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
||||
@@ -2365,47 +2371,16 @@ func TestECEncodeReplicatedVolumeSync(t *testing.T) {
|
||||
// injectNeedleToOneReplica writes a needle directly to one replica to create divergence
|
||||
func injectNeedleToOneReplica(grpcDialOption grpc.DialOption, vid needle.VolumeId, location wdclient.Location, needleId uint64, data []byte) error {
|
||||
return operation.WithVolumeServerClient(false, location.ServerAddress(), grpcDialOption, func(client volume_server_pb.VolumeServerClient) error {
|
||||
blob, size := generateNeedleBlobV3(needleId, data)
|
||||
_, err := client.WriteNeedleBlob(context.Background(), &volume_server_pb.WriteNeedleBlobRequest{
|
||||
VolumeId: uint32(vid),
|
||||
NeedleId: needleId,
|
||||
Size: size,
|
||||
NeedleBlob: blob,
|
||||
Size: int32(len(data)),
|
||||
NeedleBlob: data,
|
||||
})
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func generateNeedleBlobV3(needleId uint64, data []byte) ([]byte, int32) {
|
||||
// Overhead: DataSize(4) + Data(N) + Flags(1)
|
||||
size := 4 + len(data) + 1
|
||||
|
||||
// Create struct to calculate padding
|
||||
// Padding is based on the Size field (DataSize + Overhead)
|
||||
padding := needle.PaddingLength(types.Size(size), needle.Version3)
|
||||
|
||||
blob := make([]byte, 16+size+4+8+int(padding)) // Header(16) + Body(Size) + Checksum(4) + Timestamp(8) + Padding
|
||||
|
||||
// Header
|
||||
util.Uint32toBytes(blob[0:4], 0x12345678) // Cookie
|
||||
util.Uint64toBytes(blob[4:12], needleId)
|
||||
util.Uint32toBytes(blob[12:16], uint32(size))
|
||||
|
||||
// Body
|
||||
util.Uint32toBytes(blob[16:20], uint32(len(data)))
|
||||
copy(blob[20:], data)
|
||||
blob[20+len(data)] = 0 // Flags
|
||||
|
||||
// Checksum
|
||||
crc := needle.NewCRC(data)
|
||||
util.Uint32toBytes(blob[20+len(data)+1:20+len(data)+1+4], crc.Value())
|
||||
|
||||
// Timestamp (space reserved, filled by server)
|
||||
// Padding (zeroes)
|
||||
|
||||
return blob, int32(size)
|
||||
}
|
||||
|
||||
// uploadTestDataWithReplication uploads test data and returns the volume ID
|
||||
// using the specified collection and replication level. All files are uploaded to the same volume.
|
||||
func uploadTestDataWithReplication(t *testing.T, masterAddr string, collection string, replication string) (needle.VolumeId, error) {
|
||||
|
||||
@@ -12,20 +12,20 @@ COVERAGE_FILE := coverage.out
|
||||
# Check if weed binary exists
|
||||
check-binary:
|
||||
@if [ ! -f "$(WEED_BINARY)" ]; then \
|
||||
echo "ERROR SeaweedFS binary not found at $(WEED_BINARY)"; \
|
||||
echo "❌ SeaweedFS binary not found at $(WEED_BINARY)"; \
|
||||
echo " Please run 'make' in the root directory first"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@echo "OK SeaweedFS binary found"
|
||||
@echo "✅ SeaweedFS binary found"
|
||||
|
||||
# Check FUSE installation
|
||||
check-fuse:
|
||||
@if command -v fusermount >/dev/null 2>&1; then \
|
||||
echo "OK FUSE is installed (Linux)"; \
|
||||
echo "✅ FUSE is installed (Linux)"; \
|
||||
elif command -v umount >/dev/null 2>&1 && [ "$$(uname)" = "Darwin" ]; then \
|
||||
echo "OK FUSE is available (macOS)"; \
|
||||
echo "✅ FUSE is available (macOS)"; \
|
||||
else \
|
||||
echo "ERROR FUSE not found. Please install:"; \
|
||||
echo "❌ FUSE not found. Please install:"; \
|
||||
echo " Ubuntu/Debian: sudo apt-get install fuse"; \
|
||||
echo " CentOS/RHEL: sudo yum install fuse"; \
|
||||
echo " macOS: brew install macfuse"; \
|
||||
@@ -36,8 +36,8 @@ check-fuse:
|
||||
check-go:
|
||||
@go version | grep -q "go1\.[2-9][0-9]" || \
|
||||
go version | grep -q "go1\.2[1-9]" || \
|
||||
(echo "ERROR Go $(GO_VERSION)+ required. Current: $$(go version)" && exit 1)
|
||||
@echo "OK Go version check passed"
|
||||
(echo "❌ Go $(GO_VERSION)+ required. Current: $$(go version)" && exit 1)
|
||||
@echo "✅ Go version check passed"
|
||||
|
||||
# Verify all prerequisites
|
||||
check-prereqs: check-go check-fuse
|
||||
@@ -45,9 +45,9 @@ check-prereqs: check-go check-fuse
|
||||
|
||||
# Build the SeaweedFS binary (if needed)
|
||||
build:
|
||||
@echo "Building SeaweedFS..."
|
||||
@echo "🔨 Building SeaweedFS..."
|
||||
cd ../.. && make
|
||||
@echo "OK Build complete"
|
||||
@echo "✅ Build complete"
|
||||
|
||||
# Initialize go module (if needed)
|
||||
init-module:
|
||||
|
||||
@@ -3,7 +3,6 @@ package fuse_test
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
@@ -3,5 +3,5 @@ package fuse_test
|
||||
import "testing"
|
||||
|
||||
func TestMinimal(t *testing.T) {
|
||||
t.Log("minimal test")
|
||||
t.Log("minimal test")
|
||||
}
|
||||
|
||||
+16
-16
@@ -1,6 +1,8 @@
|
||||
module github.com/seaweedfs/seaweedfs/test/kafka
|
||||
|
||||
go 1.24.9
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.7
|
||||
|
||||
require (
|
||||
github.com/IBM/sarama v1.46.0
|
||||
@@ -14,7 +16,7 @@ require (
|
||||
replace github.com/seaweedfs/seaweedfs => ../../
|
||||
|
||||
require (
|
||||
cloud.google.com/go/auth v0.17.0 // indirect
|
||||
cloud.google.com/go/auth v0.16.5 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 // indirect
|
||||
@@ -43,21 +45,20 @@ require (
|
||||
github.com/aws/aws-sdk-go v1.55.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.18.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.93.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 // indirect
|
||||
github.com/aws/smithy-go v1.24.0 // indirect
|
||||
@@ -81,7 +82,6 @@ require (
|
||||
github.com/cronokirby/saferith v0.33.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/dropbox/dropbox-sdk-go-unofficial/v6 v6.0.5 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/eapache/go-resiliency v1.7.0 // indirect
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect
|
||||
github.com/eapache/queue v1.1.0 // indirect
|
||||
@@ -115,7 +115,7 @@ require (
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/gorilla/schema v1.4.1 // indirect
|
||||
@@ -161,7 +161,7 @@ require (
|
||||
github.com/oracle/oci-go-sdk/v65 v65.98.0 // indirect
|
||||
github.com/orcaman/concurrent-map/v2 v2.0.1 // indirect
|
||||
github.com/panjf2000/ants/v2 v2.11.3 // indirect
|
||||
github.com/parquet-go/parquet-go v0.26.3 // indirect
|
||||
github.com/parquet-go/parquet-go v0.25.1 // indirect
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pengsrc/go-shared v0.2.1-0.20190131101655-1999055a4a14 // indirect
|
||||
@@ -233,16 +233,16 @@ require (
|
||||
golang.org/x/exp v0.0.0-20250811191247-51f88131bc50 // indirect
|
||||
golang.org/x/image v0.34.0 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/oauth2 v0.34.0 // indirect
|
||||
golang.org/x/oauth2 v0.32.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/term v0.38.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
google.golang.org/api v0.258.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
google.golang.org/api v0.247.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect
|
||||
google.golang.org/grpc/security/advancedtls v1.0.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/validator.v2 v2.0.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
|
||||
+8
-21
@@ -15,7 +15,6 @@ cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOY
|
||||
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
||||
cloud.google.com/go/auth v0.16.5 h1:mFWNQ2FEVWAliEQWpAdH80omXFokmrnbDhUS9cBywsI=
|
||||
cloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ=
|
||||
cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
@@ -107,21 +106,20 @@ github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgP
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.6 h1:hFLBGUKjmLAekvi1evLi5hVvFQtSo3GYwi+Bx4lpJf8=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.6/go.mod h1:lcUL/gcd8WyjCrMnxez5OXkO3/rwcNmvfno62tnXNcI=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.6 h1:F9vWao2TwjV2MyiyVS+duza0NIRtAslgLUM0vTA1ZaE=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.6/go.mod h1:SgHzKjEVsdQr6Opor0ihgWtkWdfRAIwxYzSJ8O85VHY=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.3 h1:RIb3yr/+PZ18YYNe6MDiG/3jVoJrPmdoCARwNkMGvco=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.3/go.mod h1:jjgx1n7x0FAKl6TnakqrpkHWWKcX3xfWtdnIJs5K9CE=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.5 h1:xMo63RlqP3ZZydpJDMBsH9uJ10hgHYfQFIk1cHDXrR4=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.5/go.mod h1:hhbH6oRcou+LpXfA/0vPElh/e0M3aFeOblE1sssAAEk=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 h1:80+uETIWS1BqjnN9uJ0dBUaETh+P1XwFy5vwHwK5r9k=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16/go.mod h1:wOOsYuxYuB/7FlnVtzeBYRcjSRtQpAW0hCP7tIULMwo=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.18.4 h1:0SzCLoPRSK3qSydsaFQWugP+lOBCTPwfcBOm6222+UA=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.18.4/go.mod h1:JAet9FsBHjfdI+TnMBX4ModNNaQHAd3dc/Bk+cNsxeM=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.3/go.mod h1:br7KA6edAAqDGUYJ+zVVPAyMrPhnN+zdt17yTUT6FPw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.15 h1:NLYTEyZmVZo0Qh183sC8nC+ydJXOOeIL/qI/sS3PdLY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.15/go.mod h1:Z803iB3B0bc8oJV8zH2PERLRfQUJ2n2BXISpsA4+O1M=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
|
||||
@@ -134,10 +132,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.15 h1:wsSQ4SVz5YE1c
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.15/go.mod h1:I7sditnFGtYMIqPRU1QoHZAUrXkGp4SczmlLwrNPlD0=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.93.0 h1:IrbE3B8O9pm3lsg96AXIN5MXX4pECEuExh/A0Du3AuI=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.93.0/go.mod h1:/sJLzHtiiZvs6C1RbxS/anSAFwZD6oC6M/kotQzOiLw=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 h1:HpI7aMmJ+mm1wkSHIA2t5EaFFv5EFYXePW30p1EIrbQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4/go.mod h1:C5RdGMYGlfM0gYq/tifqgn4EbyX99V15P2V3R+VHbQU=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 h1:aM/Q24rIlS3bRAhTyFurowU8A0SMyGDtEOY/l/s/1Uw=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8/go.mod h1:+fWt2UHSb4kS7Pu8y+BMBvJF0EWx+4H0hzNwtDNRTrg=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.7 h1:eYnlt6QxnFINKzwxP5/Ucs1vkG7VT3Iezmvfgc2waUw=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.7/go.mod h1:+fWt2UHSb4kS7Pu8y+BMBvJF0EWx+4H0hzNwtDNRTrg=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 h1:AHDr0DaHIAo8c9t1emrzAlVDFp+iMMKnPdYy6XO4MCE=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12/go.mod h1:GQ73XawFFiWxyWXMHWfhiomvP3tXtdNar/fi8z18sx0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70=
|
||||
@@ -206,8 +202,6 @@ github.com/dropbox/dropbox-sdk-go-unofficial/v6 v6.0.5 h1:FT+t0UEDykcor4y3dMVKXI
|
||||
github.com/dropbox/dropbox-sdk-go-unofficial/v6 v6.0.5/go.mod h1:rSS3kM9XMzSQ6pw91Qgd6yB5jdt70N4OdtrAf74As5M=
|
||||
github.com/dsnet/try v0.0.3 h1:ptR59SsrcFUYbT/FhAbKTV6iLkeD6O18qfIWRml2fqI=
|
||||
github.com/dsnet/try v0.0.3/go.mod h1:WBM8tRpUmnXXhY1U6/S8dt6UWdHTQ7y8A5YSkRCkq40=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/eapache/go-resiliency v1.7.0 h1:n3NRTnBn5N0Cbi/IeOHuQn9s2UwVUH7Ga0ZWcP+9JTA=
|
||||
github.com/eapache/go-resiliency v1.7.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho=
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws=
|
||||
@@ -357,7 +351,6 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
|
||||
@@ -508,7 +501,6 @@ github.com/panjf2000/ants/v2 v2.11.3 h1:AfI0ngBoXJmYOpDh9m516vjqoUu2sLrIVgppI9TZ
|
||||
github.com/panjf2000/ants/v2 v2.11.3/go.mod h1:8u92CYMUc6gyvTIw8Ru7Mt7+/ESnJahz5EVtqfrilek=
|
||||
github.com/parquet-go/parquet-go v0.25.1 h1:l7jJwNM0xrk0cnIIptWMtnSnuxRkwq53S+Po3KG8Xgo=
|
||||
github.com/parquet-go/parquet-go v0.25.1/go.mod h1:AXBuotO1XiBtcqJb/FKFyjBG4aqa3aQAAWF3ZPzCanY=
|
||||
github.com/parquet-go/parquet-go v0.26.3/go.mod h1:h9GcSt41Knf5qXI1tp1TfR8bDBUtvdUMzSKe26aZcHk=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
@@ -829,7 +821,6 @@ golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4Iltr
|
||||
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
|
||||
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -937,7 +928,6 @@ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxb
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
@@ -1011,7 +1001,6 @@ google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSr
|
||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||
google.golang.org/api v0.247.0 h1:tSd/e0QrUlLsrwMKmkbQhYVa109qIintOls2Wh6bngc=
|
||||
google.golang.org/api v0.247.0/go.mod h1:r1qZOPmxXffXg6xS5uhx16Fa/UFY8QU/K4bfKrnvovM=
|
||||
google.golang.org/api v0.258.0/go.mod h1:qhOMTQEZ6lUps63ZNq9jhODswwjkjYYguA7fA3TBFww=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
@@ -1053,7 +1042,6 @@ google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
@@ -1084,7 +1072,6 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
|
||||
@@ -152,7 +152,7 @@ func testTwoConsumersRebalance(t *testing.T, addr, topicName, groupID string) {
|
||||
|
||||
// Wait for rebalancing to occur - both consumers should get new assignments
|
||||
var rebalancedAssignment1, rebalancedAssignment2 []int32
|
||||
|
||||
|
||||
// Consumer1 should get a rebalance assignment
|
||||
select {
|
||||
case partitions := <-handler1.assignments:
|
||||
@@ -372,7 +372,7 @@ func testMultipleConsumersJoin(t *testing.T, addr, topicName, groupID string) {
|
||||
t.Errorf("Partition %d assigned to multiple consumers", partition)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Each consumer should get exactly 1 partition (4 partitions / 4 consumers)
|
||||
if len(assignment) != 1 {
|
||||
t.Errorf("Consumer%d should get exactly 1 partition, got %d", i, len(assignment))
|
||||
@@ -408,7 +408,7 @@ func (h *RebalanceTestHandler) Setup(session sarama.ConsumerGroupSession) error
|
||||
h.readyOnce.Do(func() {
|
||||
close(h.ready)
|
||||
})
|
||||
|
||||
|
||||
// Send partition assignment
|
||||
partitions := make([]int32, 0)
|
||||
for topic, partitionList := range session.Claims() {
|
||||
@@ -417,13 +417,13 @@ func (h *RebalanceTestHandler) Setup(session sarama.ConsumerGroupSession) error
|
||||
partitions = append(partitions, partition)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
select {
|
||||
case h.assignments <- partitions:
|
||||
default:
|
||||
// Channel might be full, that's ok
|
||||
}
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ func TestSchemaEndToEnd_AvroRoundTrip(t *testing.T) {
|
||||
// Verify all fields
|
||||
assert.Equal(t, int32(12345), decodedMap["id"])
|
||||
assert.Equal(t, "Alice Johnson", decodedMap["name"])
|
||||
|
||||
|
||||
// Verify union fields
|
||||
emailUnion, ok := decodedMap["email"].(map[string]interface{})
|
||||
require.True(t, ok, "Email should be a union")
|
||||
@@ -126,7 +126,7 @@ func TestSchemaEndToEnd_ProtobufRoundTrip(t *testing.T) {
|
||||
require.Equal(t, uint32(2), envelope.SchemaID, "Schema ID should match")
|
||||
// Note: ParseConfluentEnvelope defaults to FormatAvro; format detection requires schema registry
|
||||
require.Equal(t, schema.FormatAvro, envelope.Format, "Format defaults to Avro without schema registry lookup")
|
||||
|
||||
|
||||
// For Protobuf with indexes, we need to use the specialized parser
|
||||
protobufEnvelope, ok := schema.ParseConfluentProtobufEnvelopeWithIndexCount(confluentMsg, 1)
|
||||
require.True(t, ok, "Message should be a valid Protobuf envelope")
|
||||
@@ -269,6 +269,7 @@ func createMockSchemaRegistryForE2E(t *testing.T) *httptest.Server {
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
func getUserAvroSchemaForE2E() string {
|
||||
return `{
|
||||
"type": "record",
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
)
|
||||
|
||||
// TestConsumerStallingPattern is a REPRODUCER for the consumer stalling bug.
|
||||
//
|
||||
//
|
||||
// This test simulates the exact pattern that causes consumers to stall:
|
||||
// 1. Consumer reads messages in batches
|
||||
// 2. Consumer commits offset after each batch
|
||||
@@ -24,7 +24,7 @@ import (
|
||||
// If the test PASSES, it means consumer successfully fetches all messages (bug fixed)
|
||||
func TestConsumerStallingPattern(t *testing.T) {
|
||||
t.Skip("REPRODUCER TEST: Requires running load test infrastructure. See comments for setup.")
|
||||
|
||||
|
||||
// This test documents the exact stalling pattern:
|
||||
// - Consumers consume messages 0-163, commit offset 163
|
||||
// - Next iteration: fetch offset 164+
|
||||
@@ -36,7 +36,7 @@ func TestConsumerStallingPattern(t *testing.T) {
|
||||
// 2. Empty fetch doesn't mean "end of partition" (could be transient)
|
||||
// 3. Consumer retries on empty fetch instead of giving up
|
||||
// 4. Logging shows why fetch stopped
|
||||
|
||||
|
||||
t.Logf("=== CONSUMER STALLING REPRODUCER ===")
|
||||
t.Logf("")
|
||||
t.Logf("Setup Steps:")
|
||||
@@ -72,27 +72,27 @@ func TestConsumerStallingPattern(t *testing.T) {
|
||||
// This is a UNIT reproducer that can run standalone
|
||||
func TestOffsetPlusOneCalculation(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
committedOffset int64
|
||||
name string
|
||||
committedOffset int64
|
||||
expectedNextOffset int64
|
||||
}{
|
||||
{"Offset 0", 0, 1},
|
||||
{"Offset 99", 99, 100},
|
||||
{"Offset 163", 163, 164}, // The exact stalling point!
|
||||
{"Offset 163", 163, 164}, // The exact stalling point!
|
||||
{"Offset 999", 999, 1000},
|
||||
{"Large offset", 10000, 10001},
|
||||
}
|
||||
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// This is the critical calculation
|
||||
nextOffset := tc.committedOffset + 1
|
||||
|
||||
|
||||
if nextOffset != tc.expectedNextOffset {
|
||||
t.Fatalf("OFFSET MATH BUG: committed=%d, next=%d (expected %d)",
|
||||
tc.committedOffset, nextOffset, tc.expectedNextOffset)
|
||||
}
|
||||
|
||||
|
||||
t.Logf("✓ offset %d → next fetch at %d", tc.committedOffset, nextOffset)
|
||||
})
|
||||
}
|
||||
@@ -105,18 +105,18 @@ func TestEmptyFetchShouldNotStopConsumer(t *testing.T) {
|
||||
// Scenario: Consumer committed offset 163, then fetches 164+
|
||||
committedOffset := int64(163)
|
||||
nextFetchOffset := committedOffset + 1
|
||||
|
||||
|
||||
// First attempt: get empty (transient - data might not be available yet)
|
||||
// WRONG behavior (bug): Consumer sees 0 bytes and stops
|
||||
// wrongConsumerLogic := (firstFetchResult == 0) // gives up!
|
||||
|
||||
|
||||
// CORRECT behavior: Consumer should retry
|
||||
correctConsumerLogic := true // continues retrying
|
||||
|
||||
correctConsumerLogic := true // continues retrying
|
||||
|
||||
if !correctConsumerLogic {
|
||||
t.Fatalf("Consumer incorrectly gave up after empty fetch at offset %d", nextFetchOffset)
|
||||
}
|
||||
|
||||
|
||||
t.Logf("✓ Empty fetch doesn't stop consumer, continues retrying")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,12 +7,11 @@
|
||||
package pb
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -14,8 +14,6 @@ import (
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/keepalive"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/mq_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/schema_pb"
|
||||
@@ -121,28 +119,22 @@ type PublisherSession struct {
|
||||
}
|
||||
|
||||
func NewDirectBrokerClient(brokerAddr string) (*DirectBrokerClient, error) {
|
||||
// Use a short-lived context for dialing so we don't store a canceled
|
||||
// context in the returned client. The client's operational context
|
||||
// (used by methods) should be cancellable independently.
|
||||
dialCtx, dialCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer dialCancel()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// Add keepalive settings; use exported server constants to keep values in sync.
|
||||
conn, err := grpc.DialContext(dialCtx, brokerAddr,
|
||||
// Add connection timeout and keepalive settings
|
||||
conn, err := grpc.DialContext(ctx, brokerAddr,
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
grpc.WithTimeout(30*time.Second),
|
||||
grpc.WithKeepaliveParams(keepalive.ClientParameters{
|
||||
Time: pb.GrpcKeepAliveTime, // align with server MinTime
|
||||
Timeout: pb.GrpcKeepAliveTimeout, // align with server timeout
|
||||
PermitWithoutStream: false, // reduce pings when idle
|
||||
Time: 30 * time.Second, // Increased from 10s to 30s
|
||||
Timeout: 10 * time.Second, // Increased from 5s to 10s
|
||||
PermitWithoutStream: false, // Changed to false to reduce pings
|
||||
}))
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, fmt.Errorf("failed to connect to broker: %v", err)
|
||||
}
|
||||
|
||||
// Create a long-lived context for the client's lifetime and store it
|
||||
// in the returned DirectBrokerClient so callers can cancel when done.
|
||||
clientCtx, clientCancel := context.WithCancel(context.Background())
|
||||
|
||||
client := mq_pb.NewSeaweedMessagingClient(conn)
|
||||
|
||||
return &DirectBrokerClient{
|
||||
@@ -150,8 +142,8 @@ func NewDirectBrokerClient(brokerAddr string) (*DirectBrokerClient, error) {
|
||||
conn: conn,
|
||||
client: client,
|
||||
publishers: make(map[string]*PublisherSession),
|
||||
ctx: clientCtx,
|
||||
cancel: clientCancel,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -205,3 +205,4 @@ func TestResumeMillionRecords_Fixed(t *testing.T) {
|
||||
|
||||
glog.Infof("🏆 MILLION RECORD KAFKA INTEGRATION TEST COMPLETED SUCCESSFULLY!")
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ func TestGatewayBasicFunctionality(t *testing.T) {
|
||||
defer gateway.CleanupAndClose()
|
||||
|
||||
addr := gateway.StartAndWait()
|
||||
|
||||
|
||||
// Give the gateway a bit more time to be fully ready
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
@@ -32,17 +32,17 @@ func TestGatewayBasicFunctionality(t *testing.T) {
|
||||
func testGatewayAcceptsConnections(t *testing.T, addr string) {
|
||||
// Test basic TCP connection to gateway
|
||||
t.Logf("Testing connection to gateway at %s", addr)
|
||||
|
||||
|
||||
conn, err := net.DialTimeout("tcp", addr, 5*time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to connect to gateway: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
|
||||
// Test that we can establish a connection and the gateway is listening
|
||||
// We don't need to send a full Kafka request for this basic test
|
||||
t.Logf("Successfully connected to gateway at %s", addr)
|
||||
|
||||
|
||||
// Optional: Test that we can write some data without error
|
||||
testData := []byte("test")
|
||||
conn.SetWriteDeadline(time.Now().Add(1 * time.Second))
|
||||
@@ -57,19 +57,19 @@ func testGatewayRefusesAfterClose(t *testing.T, gateway *testutil.GatewayTestSer
|
||||
// Get the address from the gateway's listener
|
||||
host, port := gateway.GetListenerAddr()
|
||||
addr := fmt.Sprintf("%s:%d", host, port)
|
||||
|
||||
|
||||
// Close the gateway
|
||||
gateway.CleanupAndClose()
|
||||
|
||||
|
||||
t.Log("Testing that gateway refuses connections after close")
|
||||
|
||||
|
||||
// Attempt to connect - should fail
|
||||
conn, err := net.DialTimeout("tcp", addr, 2*time.Second)
|
||||
if err == nil {
|
||||
conn.Close()
|
||||
t.Fatal("Expected connection to fail after gateway close, but it succeeded")
|
||||
}
|
||||
|
||||
|
||||
// Verify it's a connection refused error
|
||||
if !strings.Contains(err.Error(), "connection refused") && !strings.Contains(err.Error(), "connect: connection refused") {
|
||||
t.Logf("Connection failed as expected with error: %v", err)
|
||||
|
||||
@@ -1,335 +0,0 @@
|
||||
package acl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go-v2/credentials"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func getS3Client(t *testing.T) *s3.Client {
|
||||
endpoint := os.Getenv("S3_ENDPOINT")
|
||||
if endpoint == "" {
|
||||
endpoint = "http://localhost:8333"
|
||||
}
|
||||
accessKey := os.Getenv("S3_ACCESS_KEY")
|
||||
if accessKey == "" {
|
||||
accessKey = "some_access_key1"
|
||||
}
|
||||
secretKey := os.Getenv("S3_SECRET_KEY")
|
||||
if secretKey == "" {
|
||||
secretKey = "some_secret_key1"
|
||||
}
|
||||
|
||||
cfg, err := config.LoadDefaultConfig(
|
||||
context.TODO(),
|
||||
config.WithRegion("us-east-1"),
|
||||
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(
|
||||
accessKey,
|
||||
secretKey,
|
||||
"",
|
||||
)),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
return s3.NewFromConfig(cfg, func(o *s3.Options) {
|
||||
o.UsePathStyle = true
|
||||
o.BaseEndpoint = aws.String(endpoint)
|
||||
})
|
||||
}
|
||||
|
||||
func createVersionedTestBucket(t *testing.T, client *s3.Client) string {
|
||||
bucketName := "test-acl-versioned-" + strings.ToLower(strings.ReplaceAll(time.Now().Format("2006-01-02-15-04-05.000"), ":", "-"))
|
||||
_, err := client.CreateBucket(context.TODO(), &s3.CreateBucketInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = client.PutBucketVersioning(context.TODO(), &s3.PutBucketVersioningInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
VersioningConfiguration: &types.VersioningConfiguration{
|
||||
Status: types.BucketVersioningStatusEnabled,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
return bucketName
|
||||
}
|
||||
|
||||
func cleanupTestBucket(t *testing.T, client *s3.Client, bucketName string) {
|
||||
listResp, err := client.ListObjectsV2(context.TODO(), &s3.ListObjectsV2Input{
|
||||
Bucket: aws.String(bucketName),
|
||||
})
|
||||
if err == nil {
|
||||
for _, obj := range listResp.Contents {
|
||||
client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: obj.Key,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
listVersionsResp, err := client.ListObjectVersions(context.TODO(), &s3.ListObjectVersionsInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
})
|
||||
if err == nil {
|
||||
for _, version := range listVersionsResp.Versions {
|
||||
client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: version.Key,
|
||||
VersionId: version.VersionId,
|
||||
})
|
||||
}
|
||||
for _, marker := range listVersionsResp.DeleteMarkers {
|
||||
client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: marker.Key,
|
||||
VersionId: marker.VersionId,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
client.DeleteBucket(context.TODO(), &s3.DeleteBucketInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
})
|
||||
}
|
||||
|
||||
// TestGetObjectAclOnVersionedBucket tests retrieving ACL from versioned objects
|
||||
func TestGetObjectAclOnVersionedBucket(t *testing.T) {
|
||||
client := getS3Client(t)
|
||||
bucketName := createVersionedTestBucket(t, client)
|
||||
defer cleanupTestBucket(t, client, bucketName)
|
||||
|
||||
objectKey := "versioned-object-acl"
|
||||
putResp, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
Body: strings.NewReader("Hello, ACL World!"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
aclResp, err := client.GetObjectAcl(context.TODO(), &s3.GetObjectAclInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, aclResp.Owner)
|
||||
|
||||
t.Logf("Successfully retrieved ACL for versioned object %s (versionId: %s)", objectKey, *putResp.VersionId)
|
||||
}
|
||||
|
||||
// TestGetObjectAclOnSpecificVersionInVersionedBucket tests retrieving ACL for specific versions
|
||||
func TestGetObjectAclOnSpecificVersionInVersionedBucket(t *testing.T) {
|
||||
client := getS3Client(t)
|
||||
bucketName := createVersionedTestBucket(t, client)
|
||||
defer cleanupTestBucket(t, client, bucketName)
|
||||
|
||||
objectKey := "multi-version-object-acl"
|
||||
|
||||
version1Resp, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
Body: strings.NewReader("Version 1"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
version2Resp, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
Body: strings.NewReader("Version 2"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
versionId1 := *version1Resp.VersionId
|
||||
versionId2 := *version2Resp.VersionId
|
||||
|
||||
aclResp1, err := client.GetObjectAcl(context.TODO(), &s3.GetObjectAclInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
VersionId: aws.String(versionId1),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, aclResp1.Owner)
|
||||
|
||||
aclResp2, err := client.GetObjectAcl(context.TODO(), &s3.GetObjectAclInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
VersionId: aws.String(versionId2),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, aclResp2.Owner)
|
||||
|
||||
t.Logf("Successfully retrieved ACL for both versions: v1=%s, v2=%s", versionId1, versionId2)
|
||||
}
|
||||
|
||||
// TestPutObjectAclOnVersionedBucket tests setting ACL on versioned objects
|
||||
func TestPutObjectAclOnVersionedBucket(t *testing.T) {
|
||||
client := getS3Client(t)
|
||||
bucketName := createVersionedTestBucket(t, client)
|
||||
defer cleanupTestBucket(t, client, bucketName)
|
||||
|
||||
objectKey := "versioned-object-put-acl"
|
||||
putResp, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
Body: strings.NewReader("Hello, Put ACL!"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = client.PutObjectAcl(context.TODO(), &s3.PutObjectAclInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
ACL: types.ObjectCannedACLPublicRead,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
aclResp, err := client.GetObjectAcl(context.TODO(), &s3.GetObjectAclInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, aclResp.Owner)
|
||||
|
||||
t.Logf("Successfully set and verified ACL for versioned object %s (versionId: %s)", objectKey, *putResp.VersionId)
|
||||
}
|
||||
|
||||
// TestPutObjectAclOnSpecificVersionInVersionedBucket tests setting ACL on specific versions
|
||||
func TestPutObjectAclOnSpecificVersionInVersionedBucket(t *testing.T) {
|
||||
client := getS3Client(t)
|
||||
bucketName := createVersionedTestBucket(t, client)
|
||||
defer cleanupTestBucket(t, client, bucketName)
|
||||
|
||||
objectKey := "multi-version-object-put-acl"
|
||||
|
||||
version1Resp, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
Body: strings.NewReader("Version 1"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
version2Resp, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
Body: strings.NewReader("Version 2"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
versionId1 := *version1Resp.VersionId
|
||||
versionId2 := *version2Resp.VersionId
|
||||
|
||||
_, err = client.PutObjectAcl(context.TODO(), &s3.PutObjectAclInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
VersionId: aws.String(versionId1),
|
||||
ACL: types.ObjectCannedACLPublicRead,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = client.PutObjectAcl(context.TODO(), &s3.PutObjectAclInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
VersionId: aws.String(versionId2),
|
||||
ACL: types.ObjectCannedACLPrivate,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
aclResp1, err := client.GetObjectAcl(context.TODO(), &s3.GetObjectAclInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
VersionId: aws.String(versionId1),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, aclResp1.Owner)
|
||||
|
||||
aclResp2, err := client.GetObjectAcl(context.TODO(), &s3.GetObjectAclInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
VersionId: aws.String(versionId2),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, aclResp2.Owner)
|
||||
|
||||
t.Logf("Successfully set ACL on both versions: v1=%s, v2=%s", versionId1, versionId2)
|
||||
}
|
||||
|
||||
// TestModifyAclOnDifferentVersionsIndependently tests that ACL changes on one version don't affect others
|
||||
func TestModifyAclOnDifferentVersionsIndependently(t *testing.T) {
|
||||
client := getS3Client(t)
|
||||
bucketName := createVersionedTestBucket(t, client)
|
||||
defer cleanupTestBucket(t, client, bucketName)
|
||||
|
||||
objectKey := "versioned-independent-acl"
|
||||
|
||||
version1Resp, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
Body: strings.NewReader("Version 1"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
version2Resp, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
Body: strings.NewReader("Version 2"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
versionId1 := *version1Resp.VersionId
|
||||
versionId2 := *version2Resp.VersionId
|
||||
|
||||
_, err = client.PutObjectAcl(context.TODO(), &s3.PutObjectAclInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
VersionId: aws.String(versionId1),
|
||||
ACL: types.ObjectCannedACLPublicRead,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = client.PutObjectAcl(context.TODO(), &s3.PutObjectAclInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
VersionId: aws.String(versionId2),
|
||||
ACL: types.ObjectCannedACLPrivate,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
aclRespLatest, err := client.GetObjectAcl(context.TODO(), &s3.GetObjectAclInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, aclRespLatest.Owner)
|
||||
|
||||
aclResp1, err := client.GetObjectAcl(context.TODO(), &s3.GetObjectAclInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
VersionId: aws.String(versionId1),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, aclResp1.Owner)
|
||||
|
||||
aclResp2, err := client.GetObjectAcl(context.TODO(), &s3.GetObjectAclInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
VersionId: aws.String(versionId2),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, aclResp2.Owner)
|
||||
|
||||
t.Logf("Successfully verified independent ACL management across versions")
|
||||
}
|
||||
@@ -22,7 +22,10 @@ docker stop $CONTAINER_NAME || echo "already stopped"
|
||||
ulimit -n 10000
|
||||
|
||||
# Start weed w/ filer + s3 in the background
|
||||
$WEED_BIN mini \
|
||||
$WEED_BIN server \
|
||||
-filer \
|
||||
-s3 \
|
||||
-volume.max 0 \
|
||||
-master.volumeSizeLimitMB 5 \
|
||||
-dir "$(pwd)/tmp" \
|
||||
1>&2>weed.log &
|
||||
|
||||
+29
-19
@@ -63,24 +63,30 @@ start-seaweedfs: check-binary
|
||||
@pkill -f "weed volume" || true
|
||||
@pkill -f "weed filer" || true
|
||||
@pkill -f "weed s3" || true
|
||||
@pkill -f "weed mini" || true
|
||||
@sleep 2
|
||||
|
||||
# Create necessary directories
|
||||
@mkdir -p /tmp/seaweedfs-test-copying
|
||||
@mkdir -p /tmp/seaweedfs-test-copying-master
|
||||
@mkdir -p /tmp/seaweedfs-test-copying-volume
|
||||
|
||||
# Start weed mini
|
||||
@echo "Starting weed mini with dir=/tmp/seaweedfs-test-copying"
|
||||
@export AWS_ACCESS_KEY_ID=$(ACCESS_KEY) && \
|
||||
export AWS_SECRET_ACCESS_KEY=$(SECRET_KEY) && \
|
||||
# Start weed mini with S3 configuration
|
||||
@echo "Starting weed mini..."
|
||||
@nohup $(SEAWEEDFS_BINARY) mini \
|
||||
-dir=/tmp/seaweedfs-test-copying \
|
||||
-s3.port=$(S3_PORT) \
|
||||
-s3.config=/tmp/seaweedfs-s3.json \
|
||||
-ip=127.0.0.1 \
|
||||
> /tmp/seaweedfs-mini.log 2>&1 & echo $$! > /tmp/weed-mini.pid 5
|
||||
# Start master server with volume size limit
|
||||
@nohup $(SEAWEEDFS_BINARY) master -port=$(MASTER_PORT) -mdir=/tmp/seaweedfs-test-copying-master -volumeSizeLimitMB=$(VOLUME_MAX_SIZE_MB) -ip=127.0.0.1 -peers=none > /tmp/seaweedfs-master.log 2>&1 &
|
||||
@sleep 3
|
||||
|
||||
# Start volume server
|
||||
@nohup $(SEAWEEDFS_BINARY) volume -port=$(VOLUME_PORT) -master=127.0.0.1:$(MASTER_PORT) -dir=/tmp/seaweedfs-test-copying-volume -ip=127.0.0.1 > /tmp/seaweedfs-volume.log 2>&1 &
|
||||
@sleep 3
|
||||
|
||||
# Start filer server (using standard SeaweedFS gRPC port convention: HTTP port + 10000)
|
||||
@nohup $(SEAWEEDFS_BINARY) filer -port=$(FILER_PORT) -port.grpc=$$(( $(FILER_PORT) + 10000 )) -master=127.0.0.1:$(MASTER_PORT) -ip=127.0.0.1 > /tmp/seaweedfs-filer.log 2>&1 &
|
||||
@sleep 3
|
||||
|
||||
# Create S3 configuration
|
||||
@echo '{"identities":[{"name":"$(ACCESS_KEY)","credentials":[{"accessKey":"$(ACCESS_KEY)","secretKey":"$(SECRET_KEY)"}],"actions":["Admin","Read","Write"]}]}' > /tmp/seaweedfs-s3.json
|
||||
|
||||
# Start S3 server
|
||||
@nohup $(SEAWEEDFS_BINARY) s3 -port=$(S3_PORT) -filer=127.0.0.1:$(FILER_PORT) -config=/tmp/seaweedfs-s3.json -ip.bind=127.0.0.1 > /tmp/seaweedfs-s3.log 2>&1 &
|
||||
@sleep 5
|
||||
|
||||
# Wait for S3 service to be ready
|
||||
@echo "$(YELLOW)Waiting for S3 service to be ready...$(NC)"
|
||||
@@ -92,12 +98,16 @@ start-seaweedfs: check-binary
|
||||
echo "Waiting for S3 service... ($$i/30)"; \
|
||||
sleep 1; \
|
||||
done
|
||||
# Additional wait for filer gRPC to be ready
|
||||
@echo "$(YELLOW)Waiting for filer gRPC to be ready...$(NC)"
|
||||
@sleep 2
|
||||
@echo "$(GREEN)SeaweedFS server started successfully$(NC)"
|
||||
@echo "Mini Log: /tmp/seaweedfs-mini.log"
|
||||
|
||||
# Additional wait for filer gRPC to be ready
|
||||
@echo "$(YELLOW)Waiting for filer gRPC to be ready...$(NC)"
|
||||
@sleep 2
|
||||
@echo "$(GREEN)SeaweedFS server started successfully$(NC)"
|
||||
@echo "Master: http://localhost:$(MASTER_PORT)"
|
||||
@echo "Volume: http://localhost:$(VOLUME_PORT)"
|
||||
@echo "Filer: http://localhost:$(FILER_PORT)"
|
||||
@echo "S3: http://localhost:$(S3_PORT)"
|
||||
@echo "Volume Max Size: $(VOLUME_MAX_SIZE_MB)MB"
|
||||
|
||||
stop-seaweedfs:
|
||||
@echo "$(YELLOW)Stopping SeaweedFS server...$(NC)"
|
||||
|
||||
+21
-25
@@ -11,7 +11,6 @@ VOLUME_PORT := 8080
|
||||
FILER_PORT := 8888
|
||||
TEST_TIMEOUT := 10m
|
||||
TEST_PATTERN := TestCORS
|
||||
SERVER_DIR := test-mini-data
|
||||
|
||||
# Default target
|
||||
help:
|
||||
@@ -42,21 +41,21 @@ build-weed:
|
||||
@echo "Building SeaweedFS binary..."
|
||||
@cd ../../../weed && go build -o weed_binary .
|
||||
@chmod +x $(WEED_BINARY)
|
||||
@echo "OK SeaweedFS binary built at $(WEED_BINARY)"
|
||||
@echo "✅ SeaweedFS binary built at $(WEED_BINARY)"
|
||||
|
||||
check-deps: build-weed
|
||||
@echo "Checking dependencies..."
|
||||
@echo "DEBUG: Checking Go installation..."
|
||||
@echo "🔍 DEBUG: Checking Go installation..."
|
||||
@command -v go >/dev/null 2>&1 || (echo "Go is required but not installed" && exit 1)
|
||||
@echo "DEBUG: Go version: $$(go version)"
|
||||
@echo "DEBUG: Checking binary at $(WEED_BINARY)..."
|
||||
@echo "🔍 DEBUG: Go version: $$(go version)"
|
||||
@echo "🔍 DEBUG: Checking binary at $(WEED_BINARY)..."
|
||||
@test -f $(WEED_BINARY) || (echo "SeaweedFS binary not found at $(WEED_BINARY)" && exit 1)
|
||||
@echo "🔍 DEBUG: Binary size: $$(ls -lh $(WEED_BINARY) | awk '{print $$5}')"
|
||||
@echo "🔍 DEBUG: Binary permissions: $$(ls -la $(WEED_BINARY) | awk '{print $$1}')"
|
||||
@echo "🔍 DEBUG: Checking Go module dependencies..."
|
||||
@go list -m github.com/aws/aws-sdk-go-v2 >/dev/null 2>&1 || (echo "AWS SDK Go v2 not found. Run 'go mod tidy'." && exit 1)
|
||||
@go list -m github.com/stretchr/testify >/dev/null 2>&1 || (echo "Testify not found. Run 'go mod tidy'." && exit 1)
|
||||
@echo "OK All dependencies are available"
|
||||
@echo "✅ All dependencies are available"
|
||||
|
||||
# Start SeaweedFS server for testing
|
||||
start-server: check-deps
|
||||
@@ -78,26 +77,23 @@ start-server: check-deps
|
||||
@echo "🔍 DEBUG: Checking config file at ../../../docker/compose/s3.json"
|
||||
@ls -la ../../../docker/compose/s3.json || echo "⚠️ Config file not found, continuing without it"
|
||||
@echo "🔍 DEBUG: Creating volume directory..."
|
||||
@mkdir -p $(SERVER_DIR)
|
||||
@echo "🔍 DEBUG: Launching SeaweedFS S3 server in background..."
|
||||
@echo "🔍 DEBUG: Command: $(WEED_BINARY) mini -dir=$(SERVER_DIR) -s3.port=$(S3_PORT) -s3.config=$(S3_CONFIG)"
|
||||
@$(WEED_BINARY) mini \
|
||||
-dir=$(SERVER_DIR) \
|
||||
@mkdir -p ./test-volume-data
|
||||
@echo "🔍 DEBUG: Launching SeaweedFS server in background..."
|
||||
@echo "🔍 DEBUG: Command: $(WEED_BINARY) server -debug -s3 -s3.port=$(S3_PORT) -s3.allowDeleteBucketNotEmpty=true -s3.config=../../../docker/compose/s3.json -filer -filer.maxMB=64 -master.volumeSizeLimitMB=50 -volume.max=100 -dir=./test-volume-data -volume.preStopSeconds=1 -metricsPort=9324"
|
||||
@$(WEED_BINARY) server \
|
||||
-debug \
|
||||
-s3 \
|
||||
-s3.port=$(S3_PORT) \
|
||||
-s3.config=$(S3_CONFIG) \
|
||||
> weed-test.log 2>&1 & \
|
||||
echo $$! > weed-test.pid
|
||||
|
||||
@echo "Waiting for S3 server to be ready..."
|
||||
@for i in $$(seq 1 30); do \
|
||||
if echo | nc -z localhost $(S3_PORT); then \
|
||||
echo "S3 server is ready!"; \
|
||||
exit 0; \
|
||||
fi; \
|
||||
sleep 1; \
|
||||
done; \
|
||||
echo "S3 server failed to start"; \
|
||||
exit 1 > weed-server.pid
|
||||
-s3.allowDeleteBucketNotEmpty=true \
|
||||
-s3.config=../../../docker/compose/s3.json \
|
||||
-filer \
|
||||
-filer.maxMB=64 \
|
||||
-master.volumeSizeLimitMB=50 \
|
||||
-volume.max=100 \
|
||||
-dir=./test-volume-data \
|
||||
-volume.preStopSeconds=1 \
|
||||
-metricsPort=9324 \
|
||||
> weed-test.log 2>&1 & echo $$! > weed-server.pid
|
||||
@echo "🔍 DEBUG: Server PID: $$(cat weed-server.pid 2>/dev/null || echo 'PID file not found')"
|
||||
@echo "🔍 DEBUG: Checking if PID is still running..."
|
||||
@sleep 2
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
package delete
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go-v2/credentials"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
testEndpoint = "http://localhost:8333"
|
||||
testAccessKey = "admin"
|
||||
testSecretKey = "admin"
|
||||
testRegion = "us-east-1"
|
||||
)
|
||||
|
||||
func getTestClient(t *testing.T) *s3.Client {
|
||||
cfg, err := config.LoadDefaultConfig(context.TODO(),
|
||||
config.WithRegion(testRegion),
|
||||
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(
|
||||
testAccessKey,
|
||||
testSecretKey,
|
||||
"",
|
||||
)),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
client := s3.NewFromConfig(cfg, func(o *s3.Options) {
|
||||
o.BaseEndpoint = aws.String(testEndpoint)
|
||||
o.UsePathStyle = true
|
||||
})
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
func createTestBucket(t *testing.T, client *s3.Client) string {
|
||||
bucketName := fmt.Sprintf("test-multi-delete-%d", time.Now().UnixNano())
|
||||
_, err := client.CreateBucket(context.TODO(), &s3.CreateBucketInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = client.PutBucketVersioning(context.TODO(), &s3.PutBucketVersioningInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
VersioningConfiguration: &types.VersioningConfiguration{
|
||||
Status: types.BucketVersioningStatusEnabled,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return bucketName
|
||||
}
|
||||
|
||||
func cleanupBucket(t *testing.T, client *s3.Client, bucket string) {
|
||||
listResp, _ := client.ListObjectVersions(context.TODO(), &s3.ListObjectVersionsInput{
|
||||
Bucket: aws.String(bucket),
|
||||
})
|
||||
|
||||
if listResp != nil {
|
||||
var objectsToDelete []types.ObjectIdentifier
|
||||
|
||||
for _, version := range listResp.Versions {
|
||||
objectsToDelete = append(objectsToDelete, types.ObjectIdentifier{
|
||||
Key: version.Key,
|
||||
VersionId: version.VersionId,
|
||||
})
|
||||
}
|
||||
|
||||
for _, marker := range listResp.DeleteMarkers {
|
||||
objectsToDelete = append(objectsToDelete, types.ObjectIdentifier{
|
||||
Key: marker.Key,
|
||||
VersionId: marker.VersionId,
|
||||
})
|
||||
}
|
||||
|
||||
if len(objectsToDelete) > 0 {
|
||||
_, _ = client.DeleteObjects(context.TODO(), &s3.DeleteObjectsInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Delete: &types.Delete{
|
||||
Objects: objectsToDelete,
|
||||
Quiet: aws.Bool(false),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
_, _ = client.DeleteBucket(context.TODO(), &s3.DeleteBucketInput{
|
||||
Bucket: aws.String(bucket),
|
||||
})
|
||||
}
|
||||
|
||||
func TestVersioningMultiObjectDelete(t *testing.T) {
|
||||
client := getTestClient(t)
|
||||
bucket := createTestBucket(t, client)
|
||||
defer cleanupBucket(t, client, bucket)
|
||||
|
||||
key := "key"
|
||||
numVersions := 2
|
||||
var versionIds []string
|
||||
|
||||
for i := 0; i < numVersions; i++ {
|
||||
content := fmt.Sprintf("content-%d", i)
|
||||
putResp, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Key: aws.String(key),
|
||||
Body: bytes.NewReader([]byte(content)),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, putResp.VersionId)
|
||||
versionIds = append(versionIds, *putResp.VersionId)
|
||||
}
|
||||
|
||||
assert.Len(t, versionIds, 2)
|
||||
|
||||
var objectsToDelete []types.ObjectIdentifier
|
||||
for _, vid := range versionIds {
|
||||
objectsToDelete = append(objectsToDelete, types.ObjectIdentifier{
|
||||
Key: aws.String(key),
|
||||
VersionId: aws.String(vid),
|
||||
})
|
||||
}
|
||||
|
||||
deleteResp, err := client.DeleteObjects(context.TODO(), &s3.DeleteObjectsInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Delete: &types.Delete{
|
||||
Objects: objectsToDelete,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
t.Logf("Delete response: Deleted=%d, Errors=%d", len(deleteResp.Deleted), len(deleteResp.Errors))
|
||||
|
||||
listResp, err := client.ListObjectVersions(context.TODO(), &s3.ListObjectVersionsInput{
|
||||
Bucket: aws.String(bucket),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
if listResp.Versions != nil && len(listResp.Versions) > 0 {
|
||||
t.Errorf("FAIL: Expected no versions, but found %d versions:", len(listResp.Versions))
|
||||
for _, v := range listResp.Versions {
|
||||
t.Logf(" - Key=%s, VersionId=%s, IsLatest=%v", *v.Key, *v.VersionId, v.IsLatest)
|
||||
}
|
||||
} else {
|
||||
t.Logf("PASS: Versions correctly deleted")
|
||||
}
|
||||
assert.Nil(t, listResp.Versions, "Expected no versions after deletion")
|
||||
|
||||
deleteResp2, err := client.DeleteObjects(context.TODO(), &s3.DeleteObjectsInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Delete: &types.Delete{
|
||||
Objects: objectsToDelete,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, deleteResp2.Errors, "Idempotent delete should not return errors")
|
||||
}
|
||||
@@ -63,11 +63,11 @@ const (
|
||||
autoChunkSize = 8 * 1024 * 1024
|
||||
|
||||
// Test sizes
|
||||
smallFileSize = 1 * 1024 // 1KB - single chunk
|
||||
mediumFileSize = 256 * 1024 // 256KB - single chunk (at threshold)
|
||||
largeFileSize = 10 * 1024 * 1024 // 10MB - triggers auto-chunking (2 chunks)
|
||||
xlFileSize = 25 * 1024 * 1024 // 25MB - triggers auto-chunking (4 chunks)
|
||||
multipartSize = 5 * 1024 * 1024 // 5MB per part for multipart uploads
|
||||
smallFileSize = 1 * 1024 // 1KB - single chunk
|
||||
mediumFileSize = 256 * 1024 // 256KB - single chunk (at threshold)
|
||||
largeFileSize = 10 * 1024 * 1024 // 10MB - triggers auto-chunking (2 chunks)
|
||||
xlFileSize = 25 * 1024 * 1024 // 25MB - triggers auto-chunking (4 chunks)
|
||||
multipartSize = 5 * 1024 * 1024 // 5MB per part for multipart uploads
|
||||
)
|
||||
|
||||
// ETag format patterns
|
||||
@@ -540,3 +540,4 @@ func TestMultipleLargeFileUploads(t *testing.T) {
|
||||
assert.NoError(t, err, "File %d ETag should be valid hex", i)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,8 +16,6 @@ TEST_PATTERN := Test
|
||||
|
||||
# Filer group configuration
|
||||
FILER_GROUP := testgroup
|
||||
SERVER_DIR := ./test-volume-data
|
||||
S3_CONFIG := ../../../docker/compose/s3.json
|
||||
|
||||
# Default target
|
||||
help:
|
||||
@@ -46,7 +44,7 @@ build-weed:
|
||||
@echo "Building SeaweedFS binary..."
|
||||
@cd ../../../weed && go build -o weed_binary .
|
||||
@chmod +x $(WEED_BINARY)
|
||||
@echo "OK SeaweedFS binary built at $(WEED_BINARY)"
|
||||
@echo "✅ SeaweedFS binary built at $(WEED_BINARY)"
|
||||
|
||||
check-deps: build-weed
|
||||
@echo "Checking dependencies..."
|
||||
@@ -55,40 +53,51 @@ check-deps: build-weed
|
||||
@test -f $(WEED_BINARY) || (echo "SeaweedFS binary not found at $(WEED_BINARY)" && exit 1)
|
||||
@go list -m github.com/aws/aws-sdk-go-v2 >/dev/null 2>&1 || (echo "AWS SDK Go v2 not found. Run 'go mod tidy'." && exit 1)
|
||||
@go list -m github.com/stretchr/testify >/dev/null 2>&1 || (echo "Testify not found. Run 'go mod tidy'." && exit 1)
|
||||
@echo "OK All dependencies are available"
|
||||
@echo "✅ All dependencies are available"
|
||||
|
||||
# Start SeaweedFS server with filer group configured
|
||||
start-server: check-deps
|
||||
@echo "Starting SeaweedFS server with filer group: $(FILER_GROUP)..."
|
||||
@rm -f weed-server.pid
|
||||
@mkdir -p $(SERVER_DIR)
|
||||
@mkdir -p ./test-volume-data
|
||||
@if netstat -tlnp 2>/dev/null | grep $(S3_PORT) >/dev/null; then \
|
||||
echo "WARNING: Port $(S3_PORT) is already in use"; \
|
||||
echo "⚠️ Port $(S3_PORT) is already in use"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@echo "Launching SeaweedFS server with filer group $(FILER_GROUP)..."
|
||||
@export AWS_ACCESS_KEY_ID=some_access_key1 && \
|
||||
export AWS_SECRET_ACCESS_KEY=some_secret_key1 && \
|
||||
$(WEED_BINARY) mini \
|
||||
@$(WEED_BINARY) server \
|
||||
-debug \
|
||||
-dir=$(SERVER_DIR) \
|
||||
-s3 \
|
||||
-s3.port=$(S3_PORT) \
|
||||
-s3.config=$(S3_CONFIG) \
|
||||
-s3.allowDeleteBucketNotEmpty=true \
|
||||
-s3.config=../../../docker/compose/s3.json \
|
||||
-filer \
|
||||
-filer.maxMB=64 \
|
||||
-filer.filerGroup=$(FILER_GROUP) \
|
||||
> weed-server.log 2>&1 & \
|
||||
echo $$! > weed-server.pid
|
||||
|
||||
@echo "Waiting for S3 server to be ready..."
|
||||
@for i in $$(seq 1 30); do \
|
||||
if echo | nc -z localhost $(S3_PORT); then \
|
||||
echo "S3 server is ready!"; \
|
||||
-master.volumeSizeLimitMB=50 \
|
||||
-volume.max=100 \
|
||||
-dir=./test-volume-data \
|
||||
-volume.preStopSeconds=1 \
|
||||
-metricsPort=9325 \
|
||||
> weed-test.log 2>&1 & echo $$! > weed-server.pid
|
||||
@echo "Server PID: $$(cat weed-server.pid 2>/dev/null || echo 'PID file not found')"
|
||||
@echo "Waiting for server to start (up to 90 seconds)..."
|
||||
@for i in $$(seq 1 90); do \
|
||||
if curl -s http://localhost:$(S3_PORT) >/dev/null 2>&1; then \
|
||||
echo "✅ SeaweedFS server started successfully on port $(S3_PORT) with filer group $(FILER_GROUP)"; \
|
||||
exit 0; \
|
||||
fi; \
|
||||
if [ $$i -eq 30 ]; then \
|
||||
echo "⚠️ Server taking longer than expected (30s), checking logs..."; \
|
||||
if [ -f weed-test.log ]; then \
|
||||
tail -20 weed-test.log; \
|
||||
fi; \
|
||||
fi; \
|
||||
sleep 1; \
|
||||
done; \
|
||||
echo "❌ Server failed to start within 30 seconds"; \
|
||||
if [ -f weed-server.log ]; then \
|
||||
cat weed-server.log; \
|
||||
echo "❌ Server failed to start within 90 seconds"; \
|
||||
if [ -f weed-test.log ]; then \
|
||||
cat weed-test.log; \
|
||||
fi; \
|
||||
exit 1
|
||||
|
||||
@@ -117,9 +126,9 @@ stop-server:
|
||||
|
||||
# Show server logs
|
||||
logs:
|
||||
@if test -f weed-server.log; then \
|
||||
@if test -f weed-test.log; then \
|
||||
echo "=== SeaweedFS Server Logs ==="; \
|
||||
tail -f weed-server.log; \
|
||||
tail -f weed-test.log; \
|
||||
else \
|
||||
echo "No log file found. Server may not be running."; \
|
||||
fi
|
||||
@@ -137,7 +146,7 @@ test-with-server: start-server
|
||||
@echo "Test pattern: $(TEST_PATTERN)"
|
||||
@echo "Test timeout: $(TEST_TIMEOUT)"
|
||||
@trap "$(MAKE) stop-server" EXIT; \
|
||||
$(MAKE) test || (echo "❌ Tests failed, showing server logs:" && echo "=== Last 50 lines of server logs ===" && tail -50 weed-server.log && echo "=== End of server logs ===" && exit 1)
|
||||
$(MAKE) test || (echo "❌ Tests failed, showing server logs:" && echo "=== Last 50 lines of server logs ===" && tail -50 weed-test.log && echo "=== End of server logs ===" && exit 1)
|
||||
@$(MAKE) stop-server
|
||||
@echo "✅ Tests completed and server stopped"
|
||||
|
||||
@@ -145,7 +154,7 @@ test-with-server: start-server
|
||||
clean:
|
||||
@echo "Cleaning up test artifacts..."
|
||||
@$(MAKE) stop-server
|
||||
@rm -f weed-server.log weed-test*.log weed-server.pid
|
||||
@rm -f weed-test*.log weed-server.pid
|
||||
@rm -rf test-volume-data/
|
||||
@go clean -testcache
|
||||
@echo "✅ Cleanup completed"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"s3_endpoint": "http://localhost:8333",
|
||||
"master_address": "localhost:9333",
|
||||
"access_key": "some_access_key1",
|
||||
"secret_key": "some_secret_key1",
|
||||
"region": "us-east-1",
|
||||
|
||||
+63
-38
@@ -19,7 +19,6 @@ MASTER_PID_FILE = /tmp/weed-master.pid
|
||||
VOLUME_PID_FILE = /tmp/weed-volume.pid
|
||||
FILER_PID_FILE = /tmp/weed-filer.pid
|
||||
S3_PID_FILE = /tmp/weed-s3.pid
|
||||
MINI_PID_FILE = /tmp/weed-mini.pid
|
||||
|
||||
help: ## Show this help message
|
||||
@echo "SeaweedFS S3 IAM Integration Tests"
|
||||
@@ -50,54 +49,80 @@ test: clean setup start-services run-tests stop-services ## Run complete IAM int
|
||||
test-quick: run-tests ## Run tests assuming services are already running
|
||||
|
||||
run-tests: ## Execute the Go tests
|
||||
@echo "Running S3 IAM Integration Tests..."
|
||||
@echo "🧪 Running S3 IAM Integration Tests..."
|
||||
go test -v -timeout $(TEST_TIMEOUT) ./...
|
||||
|
||||
setup: ## Setup test environment
|
||||
@echo "Setting up test environment..."
|
||||
@echo "🔧 Setting up test environment..."
|
||||
@mkdir -p test-volume-data/filerldb2
|
||||
@mkdir -p test-volume-data/m9333
|
||||
|
||||
start-services: ## Start SeaweedFS services for testing
|
||||
@echo "Starting SeaweedFS services using weed mini..."
|
||||
@mkdir -p test-volume-data
|
||||
@$(WEED_BINARY) mini \
|
||||
-dir=test-volume-data \
|
||||
-s3.port=$(S3_PORT) \
|
||||
-s3.config=test_config.json \
|
||||
-s3.iam.config=$(CURDIR)/iam_config.json \
|
||||
> weed-mini.log 2>&1 & \
|
||||
echo $$! > $(MINI_PID_FILE)
|
||||
@echo "🚀 Starting SeaweedFS services..."
|
||||
@echo "Starting master server..."
|
||||
@$(WEED_BINARY) master -port=$(MASTER_PORT) \
|
||||
-mdir=test-volume-data/m9333 \
|
||||
-peers=none > weed-master.log 2>&1 & \
|
||||
echo $$! > $(MASTER_PID_FILE)
|
||||
|
||||
@echo "Waiting for services to be ready..."
|
||||
@$(MAKE) wait-for-services
|
||||
@echo "OK All services started and ready"
|
||||
@echo "Waiting for master server to be ready..."
|
||||
@timeout 60 bash -c 'until curl -s http://localhost:$(MASTER_PORT)/cluster/status > /dev/null 2>&1; do echo "Waiting for master server..."; sleep 2; done' || (echo "❌ Master failed to start, checking logs..." && tail -20 weed-master.log && exit 1)
|
||||
@echo "✅ Master server is ready"
|
||||
|
||||
@echo "Starting volume server..."
|
||||
@$(WEED_BINARY) volume -port=$(VOLUME_PORT) \
|
||||
-ip=localhost \
|
||||
-dataCenter=dc1 -rack=rack1 \
|
||||
-dir=test-volume-data \
|
||||
-max=100 \
|
||||
-master=localhost:$(MASTER_PORT) > weed-volume.log 2>&1 & \
|
||||
echo $$! > $(VOLUME_PID_FILE)
|
||||
|
||||
@echo "Waiting for volume server to be ready..."
|
||||
@timeout 60 bash -c 'until curl -s http://localhost:$(VOLUME_PORT)/status > /dev/null 2>&1; do echo "Waiting for volume server..."; sleep 2; done' || (echo "❌ Volume server failed to start, checking logs..." && tail -20 weed-volume.log && exit 1)
|
||||
@echo "✅ Volume server is ready"
|
||||
|
||||
@echo "Starting filer server..."
|
||||
@$(WEED_BINARY) filer -port=$(FILER_PORT) \
|
||||
-defaultStoreDir=test-volume-data/filerldb2 \
|
||||
-master=localhost:$(MASTER_PORT) > weed-filer.log 2>&1 & \
|
||||
echo $$! > $(FILER_PID_FILE)
|
||||
|
||||
@echo "Waiting for filer server to be ready..."
|
||||
@timeout 60 bash -c 'until curl -s http://localhost:$(FILER_PORT)/status > /dev/null 2>&1; do echo "Waiting for filer server..."; sleep 2; done' || (echo "❌ Filer failed to start, checking logs..." && tail -20 weed-filer.log && exit 1)
|
||||
@echo "✅ Filer server is ready"
|
||||
|
||||
@echo "Starting S3 API server with IAM..."
|
||||
@$(WEED_BINARY) -v=3 s3 -port=$(S3_PORT) \
|
||||
-filer=localhost:$(FILER_PORT) \
|
||||
-config=test_config.json \
|
||||
-iam.config=$(CURDIR)/iam_config.json > weed-s3.log 2>&1 & \
|
||||
echo $$! > $(S3_PID_FILE)
|
||||
|
||||
@echo "Waiting for S3 API server to be ready..."
|
||||
@timeout 60 bash -c 'until curl -s http://localhost:$(S3_PORT) > /dev/null 2>&1; do echo "Waiting for S3 API server..."; sleep 2; done' || (echo "❌ S3 API failed to start, checking logs..." && tail -20 weed-s3.log && exit 1)
|
||||
@echo "✅ S3 API server is ready"
|
||||
|
||||
@echo "✅ All services started and ready"
|
||||
|
||||
wait-for-services: ## Wait for all services to be ready
|
||||
@echo "Waiting for services to be ready..."
|
||||
@echo "⏳ Waiting for services to be ready..."
|
||||
@echo "Checking master server..."
|
||||
@timeout 30 bash -c 'until curl -s http://localhost:$(MASTER_PORT)/cluster/status > /dev/null; do sleep 1; done' || (echo "❌ Master failed to start" && exit 1)
|
||||
|
||||
@echo "Checking filer server..."
|
||||
@timeout 30 bash -c 'until curl -s http://localhost:$(FILER_PORT)/status > /dev/null; do sleep 1; done' || (echo "❌ Filer failed to start" && exit 1)
|
||||
|
||||
@echo "Checking S3 API server..."
|
||||
@for i in $$(seq 1 30); do \
|
||||
if curl -s http://localhost:$(S3_PORT) > /dev/null 2>&1; then \
|
||||
echo "OK S3 API server is ready"; \
|
||||
exit 0; \
|
||||
fi; \
|
||||
sleep 1; \
|
||||
done; \
|
||||
echo "ERROR S3 API failed to start"; \
|
||||
exit 1
|
||||
@timeout 30 bash -c 'until curl -s http://localhost:$(S3_PORT) > /dev/null 2>&1; do sleep 1; done' || (echo "❌ S3 API failed to start" && exit 1)
|
||||
|
||||
@echo "Pre-allocating volumes for concurrent operations..."
|
||||
@curl -s "http://localhost:$(MASTER_PORT)/vol/grow?collection=default&count=10&replication=000" > /dev/null || echo "WARNING Volume pre-allocation failed, but continuing..."
|
||||
@curl -s "http://localhost:$(MASTER_PORT)/vol/grow?collection=default&count=10&replication=000" > /dev/null || echo "⚠️ Volume pre-allocation failed, but continuing..."
|
||||
@sleep 3
|
||||
@echo "OK All services are ready"
|
||||
@echo "✅ All services are ready"
|
||||
|
||||
stop-services: ## Stop all SeaweedFS services
|
||||
@echo "Stopping SeaweedFS services..."
|
||||
@if [ -f $(MINI_PID_FILE) ]; then \
|
||||
echo "Stopping weed mini..."; \
|
||||
kill $$(cat $(MINI_PID_FILE)) 2>/dev/null || true; \
|
||||
rm -f $(MINI_PID_FILE); \
|
||||
fi
|
||||
@echo "🛑 Stopping SeaweedFS services..."
|
||||
@if [ -f $(S3_PID_FILE) ]; then \
|
||||
echo "Stopping S3 API server..."; \
|
||||
kill $$(cat $(S3_PID_FILE)) 2>/dev/null || true; \
|
||||
@@ -118,17 +143,17 @@ stop-services: ## Stop all SeaweedFS services
|
||||
kill $$(cat $(MASTER_PID_FILE)) 2>/dev/null || true; \
|
||||
rm -f $(MASTER_PID_FILE); \
|
||||
fi
|
||||
@echo "OK All services stopped"
|
||||
@echo "✅ All services stopped"
|
||||
|
||||
clean: stop-services ## Clean up test environment
|
||||
@echo "Cleaning up test environment..."
|
||||
@echo "🧹 Cleaning up test environment..."
|
||||
@rm -rf test-volume-data
|
||||
@rm -f weed-*.log
|
||||
@rm -f *.test
|
||||
@echo "Cleanup complete"
|
||||
@echo "✅ Cleanup complete"
|
||||
|
||||
logs: ## Show service logs
|
||||
@echo "Service Logs:"
|
||||
@echo "📋 Service Logs:"
|
||||
@echo "=== Master Log ==="
|
||||
@tail -20 weed-master.log 2>/dev/null || echo "No master log"
|
||||
@echo ""
|
||||
@@ -142,7 +167,7 @@ logs: ## Show service logs
|
||||
@tail -20 weed-s3.log 2>/dev/null || echo "No S3 log"
|
||||
|
||||
status: ## Check service status
|
||||
@echo "Service Status:"
|
||||
@echo "📊 Service Status:"
|
||||
@echo -n "Master: "; curl -s http://localhost:$(MASTER_PORT)/cluster/status > /dev/null 2>&1 && echo "✅ Running" || echo "❌ Not running"
|
||||
@echo -n "Filer: "; curl -s http://localhost:$(FILER_PORT)/status > /dev/null 2>&1 && echo "✅ Running" || echo "❌ Not running"
|
||||
@echo -n "S3 API: "; curl -s http://localhost:$(S3_PORT) > /dev/null 2>&1 && echo "✅ Running" || echo "❌ Not running"
|
||||
|
||||
@@ -443,7 +443,7 @@ func TestS3IAMBucketPolicyIntegration(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, testObjectData, string(data))
|
||||
result.Body.Close()
|
||||
|
||||
|
||||
// Clean up bucket policy after this test
|
||||
_, err = adminClient.DeleteBucketPolicy(&s3.DeleteBucketPolicyInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
@@ -481,7 +481,7 @@ func TestS3IAMBucketPolicyIntegration(t *testing.T) {
|
||||
assert.Contains(t, *policyResult.Policy, "Deny")
|
||||
|
||||
// NOTE: Enforcement test is commented out due to known architectural limitation:
|
||||
//
|
||||
//
|
||||
// KNOWN LIMITATION: DeleteObject uses the coarse-grained ACTION_WRITE constant,
|
||||
// which convertActionToS3Format maps to "s3:PutObject" (not "s3:DeleteObject").
|
||||
// This means the policy engine evaluates the deny policy against "s3:PutObject",
|
||||
@@ -499,7 +499,7 @@ func TestS3IAMBucketPolicyIntegration(t *testing.T) {
|
||||
// awsErr, ok := err.(awserr.Error)
|
||||
// require.True(t, ok, "Error should be an awserr.Error")
|
||||
// assert.Equal(t, "AccessDenied", awsErr.Code(), "Expected AccessDenied error code")
|
||||
|
||||
|
||||
// Clean up bucket policy after this test
|
||||
_, err = adminClient.DeleteBucketPolicy(&s3.DeleteBucketPolicyInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
|
||||
@@ -1,431 +0,0 @@
|
||||
package iam
|
||||
|
||||
// Integration tests for SeaweedFS service accounts.
|
||||
// These tests ensure comprehensive coverage of service account functionality
|
||||
// including security, access control, and expiration.
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestServiceAccountS3Access verifies that service accounts can actually
|
||||
// perform S3 operations using their credentials.
|
||||
func TestServiceAccountS3Access(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test in short mode")
|
||||
}
|
||||
|
||||
if !isSeaweedFSRunning(t) {
|
||||
t.Skip("SeaweedFS is not running at", TestIAMEndpoint)
|
||||
}
|
||||
|
||||
// Setup: Create a parent user
|
||||
parentUserName := fmt.Sprintf("s3access-test-%d", time.Now().UnixNano())
|
||||
|
||||
// Create parent user
|
||||
resp, err := callIAMAPI(t, "CreateUser", url.Values{
|
||||
"UserName": {parentUserName},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
resp.Body.Close()
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode, "Failed to create parent user")
|
||||
|
||||
defer func() {
|
||||
// Cleanup: delete parent user
|
||||
callIAMAPI(t, "DeleteUser", url.Values{"UserName": {parentUserName}})
|
||||
}()
|
||||
|
||||
// Create service account for the parent user
|
||||
createResp, err := callIAMAPI(t, "CreateServiceAccount", url.Values{
|
||||
"ParentUser": {parentUserName},
|
||||
"Description": {"S3 Access Test Service Account"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer createResp.Body.Close()
|
||||
require.Equal(t, http.StatusOK, createResp.StatusCode, "Failed to create service account")
|
||||
|
||||
body, err := io.ReadAll(createResp.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
var saResp CreateServiceAccountResponse
|
||||
err = xml.Unmarshal(body, &saResp)
|
||||
require.NoError(t, err, "Failed to parse CreateServiceAccount response: %s", string(body))
|
||||
|
||||
accessKeyId := saResp.CreateServiceAccountResult.ServiceAccount.AccessKeyId
|
||||
secretAccessKey := saResp.CreateServiceAccountResult.ServiceAccount.SecretAccessKey
|
||||
saId := saResp.CreateServiceAccountResult.ServiceAccount.ServiceAccountId
|
||||
|
||||
require.NotEmpty(t, accessKeyId, "AccessKeyId should not be empty")
|
||||
require.NotEmpty(t, secretAccessKey, "SecretAccessKey should not be empty")
|
||||
|
||||
defer func() {
|
||||
// Cleanup: delete service account
|
||||
callIAMAPI(t, "DeleteServiceAccount", url.Values{"ServiceAccountId": {saId}})
|
||||
}()
|
||||
|
||||
t.Run("list_buckets_with_sa_credentials", func(t *testing.T) {
|
||||
sess, err := session.NewSession(&aws.Config{
|
||||
Region: aws.String("us-east-1"),
|
||||
Endpoint: aws.String(TestIAMEndpoint),
|
||||
Credentials: credentials.NewStaticCredentials(
|
||||
accessKeyId,
|
||||
secretAccessKey,
|
||||
"",
|
||||
),
|
||||
DisableSSL: aws.Bool(true),
|
||||
S3ForcePathStyle: aws.Bool(true),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
s3Client := s3.New(sess)
|
||||
_, err = s3Client.ListBuckets(&s3.ListBucketsInput{})
|
||||
|
||||
// We don't necessarily expect success (depends on permissions),
|
||||
// but we should NOT get InvalidAccessKeyId or SignatureDoesNotMatch
|
||||
if err != nil {
|
||||
if aerr, ok := err.(awserr.Error); ok {
|
||||
assert.NotEqual(t, "InvalidAccessKeyId", aerr.Code(),
|
||||
"Service account credentials should be recognized")
|
||||
assert.NotEqual(t, "SignatureDoesNotMatch", aerr.Code(),
|
||||
"Service account signature should be valid")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("create_bucket_with_sa_credentials", func(t *testing.T) {
|
||||
sess, err := session.NewSession(&aws.Config{
|
||||
Region: aws.String("us-east-1"),
|
||||
Endpoint: aws.String(TestIAMEndpoint),
|
||||
Credentials: credentials.NewStaticCredentials(
|
||||
accessKeyId,
|
||||
secretAccessKey,
|
||||
"",
|
||||
),
|
||||
DisableSSL: aws.Bool(true),
|
||||
S3ForcePathStyle: aws.Bool(true),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
s3Client := s3.New(sess)
|
||||
bucketName := fmt.Sprintf("sa-test-bucket-%d", time.Now().UnixNano())
|
||||
|
||||
_, err = s3Client.CreateBucket(&s3.CreateBucketInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
})
|
||||
|
||||
// Check that we get a proper response (success or AccessDenied based on policy)
|
||||
// but NOT InvalidAccessKeyId
|
||||
if err != nil {
|
||||
if aerr, ok := err.(awserr.Error); ok {
|
||||
assert.NotEqual(t, "InvalidAccessKeyId", aerr.Code(),
|
||||
"Service account credentials should be recognized")
|
||||
assert.NotEqual(t, "SignatureDoesNotMatch", aerr.Code(),
|
||||
"Service account signature should be valid")
|
||||
}
|
||||
} else {
|
||||
// Cleanup if bucket was created
|
||||
defer s3Client.DeleteBucket(&s3.DeleteBucketInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestServiceAccountExpiration verifies that expired service accounts
|
||||
// are properly rejected.
|
||||
func TestServiceAccountExpiration(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test in short mode")
|
||||
}
|
||||
|
||||
if !isSeaweedFSRunning(t) {
|
||||
t.Skip("SeaweedFS is not running at", TestIAMEndpoint)
|
||||
}
|
||||
|
||||
// Setup: Create a parent user
|
||||
parentUserName := fmt.Sprintf("expiry-test-%d", time.Now().UnixNano())
|
||||
|
||||
resp, err := callIAMAPI(t, "CreateUser", url.Values{
|
||||
"UserName": {parentUserName},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
resp.Body.Close()
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
defer func() {
|
||||
callIAMAPI(t, "DeleteUser", url.Values{"UserName": {parentUserName}})
|
||||
}()
|
||||
|
||||
t.Run("reject_past_expiration", func(t *testing.T) {
|
||||
// Try to create a service account with expiration in the past
|
||||
pastExpiration := time.Now().Add(-1 * time.Hour).Unix()
|
||||
|
||||
createResp, err := callIAMAPI(t, "CreateServiceAccount", url.Values{
|
||||
"ParentUser": {parentUserName},
|
||||
"Description": {"Should fail - past expiration"},
|
||||
"Expiration": {strconv.FormatInt(pastExpiration, 10)},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer createResp.Body.Close()
|
||||
|
||||
// Should fail because expiration is in the past
|
||||
assert.NotEqual(t, http.StatusOK, createResp.StatusCode,
|
||||
"Creating service account with past expiration should fail")
|
||||
})
|
||||
|
||||
t.Run("accept_future_expiration", func(t *testing.T) {
|
||||
// Create a service account with expiration in the future
|
||||
futureExpiration := time.Now().Add(24 * time.Hour).Unix()
|
||||
|
||||
createResp, err := callIAMAPI(t, "CreateServiceAccount", url.Values{
|
||||
"ParentUser": {parentUserName},
|
||||
"Description": {"Should succeed - future expiration"},
|
||||
"Expiration": {strconv.FormatInt(futureExpiration, 10)},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer createResp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusOK, createResp.StatusCode,
|
||||
"Creating service account with future expiration should succeed")
|
||||
|
||||
// Parse response to get service account ID for cleanup
|
||||
if createResp.StatusCode == http.StatusOK {
|
||||
body, err := io.ReadAll(createResp.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
var saResp CreateServiceAccountResponse
|
||||
require.NoError(t, xml.Unmarshal(body, &saResp))
|
||||
|
||||
saId := saResp.CreateServiceAccountResult.ServiceAccount.ServiceAccountId
|
||||
if saId != "" {
|
||||
t.Cleanup(func() {
|
||||
callIAMAPI(t, "DeleteServiceAccount", url.Values{
|
||||
"ServiceAccountId": {saId},
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("reject_past_expiration_on_update", func(t *testing.T) {
|
||||
// Create a valid service account first
|
||||
futureExpiration := time.Now().Add(24 * time.Hour).Unix()
|
||||
|
||||
createResp, err := callIAMAPI(t, "CreateServiceAccount", url.Values{
|
||||
"ParentUser": {parentUserName},
|
||||
"Description": {"For update test"},
|
||||
"Expiration": {strconv.FormatInt(futureExpiration, 10)},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer createResp.Body.Close()
|
||||
require.Equal(t, http.StatusOK, createResp.StatusCode)
|
||||
|
||||
body, err := io.ReadAll(createResp.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
var saResp CreateServiceAccountResponse
|
||||
err = xml.Unmarshal(body, &saResp)
|
||||
require.NoError(t, err)
|
||||
|
||||
saId := saResp.CreateServiceAccountResult.ServiceAccount.ServiceAccountId
|
||||
require.NotEmpty(t, saId)
|
||||
|
||||
defer func() {
|
||||
callIAMAPI(t, "DeleteServiceAccount", url.Values{"ServiceAccountId": {saId}})
|
||||
}()
|
||||
|
||||
// Try to update with past expiration
|
||||
pastExpiration := time.Now().Add(-1 * time.Hour).Unix()
|
||||
|
||||
updateResp, err := callIAMAPI(t, "UpdateServiceAccount", url.Values{
|
||||
"ServiceAccountId": {saId},
|
||||
"Expiration": {strconv.FormatInt(pastExpiration, 10)},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer updateResp.Body.Close()
|
||||
|
||||
// Should fail because expiration is in the past
|
||||
assert.NotEqual(t, http.StatusOK, updateResp.StatusCode,
|
||||
"Updating service account with past expiration should fail")
|
||||
})
|
||||
}
|
||||
|
||||
// TestServiceAccountInheritedPermissions verifies that service accounts
|
||||
// inherit their parent user's permissions.
|
||||
// This is a key security test - SAs should not have MORE permissions than parent.
|
||||
func TestServiceAccountInheritedPermissions(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test in short mode")
|
||||
}
|
||||
|
||||
if !isSeaweedFSRunning(t) {
|
||||
t.Skip("SeaweedFS is not running at", TestIAMEndpoint)
|
||||
}
|
||||
|
||||
// Setup: Create a parent user
|
||||
parentUserName := fmt.Sprintf("inherit-test-%d", time.Now().UnixNano())
|
||||
|
||||
resp, err := callIAMAPI(t, "CreateUser", url.Values{
|
||||
"UserName": {parentUserName},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
resp.Body.Close()
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
defer func() {
|
||||
callIAMAPI(t, "DeleteUser", url.Values{"UserName": {parentUserName}})
|
||||
}()
|
||||
|
||||
// Create service account
|
||||
createResp, err := callIAMAPI(t, "CreateServiceAccount", url.Values{
|
||||
"ParentUser": {parentUserName},
|
||||
"Description": {"Permissions inheritance test"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer createResp.Body.Close()
|
||||
require.Equal(t, http.StatusOK, createResp.StatusCode)
|
||||
|
||||
body, err := io.ReadAll(createResp.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
var saResp CreateServiceAccountResponse
|
||||
err = xml.Unmarshal(body, &saResp)
|
||||
require.NoError(t, err)
|
||||
|
||||
saId := saResp.CreateServiceAccountResult.ServiceAccount.ServiceAccountId
|
||||
require.NotEmpty(t, saId)
|
||||
|
||||
defer func() {
|
||||
callIAMAPI(t, "DeleteServiceAccount", url.Values{"ServiceAccountId": {saId}})
|
||||
}()
|
||||
|
||||
t.Run("service_account_linked_to_parent", func(t *testing.T) {
|
||||
// Verify the service account is correctly linked to the parent
|
||||
getResp, err := callIAMAPI(t, "GetServiceAccount", url.Values{
|
||||
"ServiceAccountId": {saId},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer getResp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(getResp.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
var result GetServiceAccountResponse
|
||||
err = xml.Unmarshal(body, &result)
|
||||
require.NoError(t, err, "Failed to parse response: %s", string(body))
|
||||
|
||||
assert.Equal(t, parentUserName, result.GetServiceAccountResult.ServiceAccount.ParentUser,
|
||||
"Service account should be linked to correct parent user")
|
||||
})
|
||||
|
||||
t.Run("list_shows_correct_parent", func(t *testing.T) {
|
||||
// List service accounts filtered by parent
|
||||
listResp, err := callIAMAPI(t, "ListServiceAccounts", url.Values{
|
||||
"ParentUser": {parentUserName},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer listResp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(listResp.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
var listResult ListServiceAccountsResponse
|
||||
err = xml.Unmarshal(body, &listResult)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should find at least one service account for this parent
|
||||
found := false
|
||||
for _, sa := range listResult.ListServiceAccountsResult.ServiceAccounts {
|
||||
if sa.ServiceAccountId == saId {
|
||||
found = true
|
||||
assert.Equal(t, parentUserName, sa.ParentUser)
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, found, "Service account should appear in list filtered by parent")
|
||||
})
|
||||
}
|
||||
|
||||
// TestServiceAccountAccessKeyFormat verifies that service account access keys
|
||||
// follow the correct AWS format.
|
||||
func TestServiceAccountAccessKeyFormat(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test in short mode")
|
||||
}
|
||||
|
||||
if !isSeaweedFSRunning(t) {
|
||||
t.Skip("SeaweedFS is not running at", TestIAMEndpoint)
|
||||
}
|
||||
|
||||
// Setup: Create a parent user
|
||||
parentUserName := fmt.Sprintf("keyformat-test-%d", time.Now().UnixNano())
|
||||
|
||||
resp, err := callIAMAPI(t, "CreateUser", url.Values{
|
||||
"UserName": {parentUserName},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
resp.Body.Close()
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
defer func() {
|
||||
callIAMAPI(t, "DeleteUser", url.Values{"UserName": {parentUserName}})
|
||||
}()
|
||||
|
||||
createResp, err := callIAMAPI(t, "CreateServiceAccount", url.Values{
|
||||
"ParentUser": {parentUserName},
|
||||
"Description": {"Key format test"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer createResp.Body.Close()
|
||||
require.Equal(t, http.StatusOK, createResp.StatusCode)
|
||||
|
||||
body, err := io.ReadAll(createResp.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
var saResp CreateServiceAccountResponse
|
||||
err = xml.Unmarshal(body, &saResp)
|
||||
require.NoError(t, err)
|
||||
|
||||
accessKeyId := saResp.CreateServiceAccountResult.ServiceAccount.AccessKeyId
|
||||
secretAccessKey := saResp.CreateServiceAccountResult.ServiceAccount.SecretAccessKey
|
||||
saId := saResp.CreateServiceAccountResult.ServiceAccount.ServiceAccountId
|
||||
|
||||
defer func() {
|
||||
callIAMAPI(t, "DeleteServiceAccount", url.Values{"ServiceAccountId": {saId}})
|
||||
}()
|
||||
|
||||
t.Run("access_key_has_correct_prefix", func(t *testing.T) {
|
||||
// Service account access keys should start with ABIA
|
||||
assert.True(t, len(accessKeyId) >= 4,
|
||||
"Access key should be at least 4 characters")
|
||||
assert.Equal(t, "ABIA", accessKeyId[:4],
|
||||
"Service account access key should start with ABIA prefix")
|
||||
})
|
||||
|
||||
t.Run("access_key_has_correct_length", func(t *testing.T) {
|
||||
// AWS access keys are 20 characters
|
||||
assert.Equal(t, 20, len(accessKeyId),
|
||||
"Access key should be exactly 20 characters (AWS standard)")
|
||||
})
|
||||
|
||||
t.Run("secret_key_has_correct_length", func(t *testing.T) {
|
||||
// AWS secret keys are 40 characters
|
||||
assert.Equal(t, 40, len(secretAccessKey),
|
||||
"Secret key should be exactly 40 characters (AWS standard)")
|
||||
})
|
||||
}
|
||||
@@ -1,363 +0,0 @@
|
||||
package iam
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Service Account API test constants
|
||||
const (
|
||||
TestIAMEndpoint = "http://localhost:8333"
|
||||
)
|
||||
|
||||
// ServiceAccountInfo represents the response structure for service account operations
|
||||
type ServiceAccountInfo struct {
|
||||
ServiceAccountId string `xml:"ServiceAccountId"`
|
||||
ParentUser string `xml:"ParentUser"`
|
||||
Description string `xml:"Description,omitempty"`
|
||||
AccessKeyId string `xml:"AccessKeyId"`
|
||||
SecretAccessKey string `xml:"SecretAccessKey,omitempty"`
|
||||
Status string `xml:"Status"`
|
||||
Expiration string `xml:"Expiration,omitempty"`
|
||||
CreateDate string `xml:"CreateDate"`
|
||||
}
|
||||
|
||||
// CreateServiceAccountResponse represents the response for CreateServiceAccount
|
||||
type CreateServiceAccountResponse struct {
|
||||
XMLName xml.Name `xml:"CreateServiceAccountResponse"`
|
||||
CreateServiceAccountResult struct {
|
||||
ServiceAccount ServiceAccountInfo `xml:"ServiceAccount"`
|
||||
} `xml:"CreateServiceAccountResult"`
|
||||
}
|
||||
|
||||
// ListServiceAccountsResponse represents the response for ListServiceAccounts
|
||||
type ListServiceAccountsResponse struct {
|
||||
XMLName xml.Name `xml:"ListServiceAccountsResponse"`
|
||||
ListServiceAccountsResult struct {
|
||||
ServiceAccounts []ServiceAccountInfo `xml:"ServiceAccounts>member"`
|
||||
IsTruncated bool `xml:"IsTruncated"`
|
||||
} `xml:"ListServiceAccountsResult"`
|
||||
}
|
||||
|
||||
// GetServiceAccountResponse represents the response for GetServiceAccount
|
||||
type GetServiceAccountResponse struct {
|
||||
XMLName xml.Name `xml:"GetServiceAccountResponse"`
|
||||
GetServiceAccountResult struct {
|
||||
ServiceAccount ServiceAccountInfo `xml:"ServiceAccount"`
|
||||
} `xml:"GetServiceAccountResult"`
|
||||
}
|
||||
|
||||
// TestServiceAccountLifecycle tests the complete lifecycle of service accounts
|
||||
// This is a high-value test covering Create, Get, List, Update, Delete operations
|
||||
func TestServiceAccountLifecycle(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test in short mode")
|
||||
}
|
||||
|
||||
// Check if SeaweedFS is running
|
||||
if !isSeaweedFSRunning(t) {
|
||||
t.Skip("SeaweedFS is not running at", TestIAMEndpoint)
|
||||
}
|
||||
|
||||
// First, ensure the parent user exists
|
||||
parentUserName := fmt.Sprintf("testuser-%d", time.Now().UnixNano())
|
||||
|
||||
t.Run("create_parent_user", func(t *testing.T) {
|
||||
resp, err := callIAMAPI(t, "CreateUser", url.Values{
|
||||
"UserName": {parentUserName},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode, "CreateUser should succeed")
|
||||
})
|
||||
|
||||
// Store service account IDs for cleanup
|
||||
var createdServiceAccounts []string
|
||||
|
||||
defer func() {
|
||||
// Cleanup: delete all service accounts first, then parent user
|
||||
for _, saId := range createdServiceAccounts {
|
||||
resp, _ := callIAMAPI(t, "DeleteServiceAccount", url.Values{
|
||||
"ServiceAccountId": {saId},
|
||||
})
|
||||
if resp != nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
}
|
||||
// Now delete the parent user
|
||||
resp, _ := callIAMAPI(t, "DeleteUser", url.Values{
|
||||
"UserName": {parentUserName},
|
||||
})
|
||||
if resp != nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
var createdSAId string
|
||||
var createdAccessKeyId string
|
||||
var createdSecretAccessKey string
|
||||
|
||||
t.Run("create_service_account", func(t *testing.T) {
|
||||
resp, err := callIAMAPI(t, "CreateServiceAccount", url.Values{
|
||||
"ParentUser": {parentUserName},
|
||||
"Description": {"Test service account for CI/CD"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode, "CreateServiceAccount should succeed")
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
var createResp CreateServiceAccountResponse
|
||||
err = xml.Unmarshal(body, &createResp)
|
||||
require.NoError(t, err)
|
||||
|
||||
sa := createResp.CreateServiceAccountResult.ServiceAccount
|
||||
createdSAId = sa.ServiceAccountId
|
||||
createdAccessKeyId = sa.AccessKeyId
|
||||
createdSecretAccessKey = sa.SecretAccessKey
|
||||
|
||||
// Add to cleanup list
|
||||
createdServiceAccounts = append(createdServiceAccounts, createdSAId)
|
||||
|
||||
assert.NotEmpty(t, createdSAId, "ServiceAccountId should not be empty")
|
||||
assert.Equal(t, parentUserName, sa.ParentUser, "ParentUser should match")
|
||||
assert.Equal(t, "Test service account for CI/CD", sa.Description)
|
||||
assert.Equal(t, "Active", sa.Status)
|
||||
assert.NotEmpty(t, sa.AccessKeyId, "AccessKeyId should not be empty")
|
||||
assert.NotEmpty(t, sa.SecretAccessKey, "SecretAccessKey should be returned on create")
|
||||
assert.True(t, strings.HasPrefix(sa.AccessKeyId, "ABIA"),
|
||||
"Service account AccessKeyId should have ABIA prefix")
|
||||
|
||||
t.Logf("Created service account: ID=%s, AccessKeyId=%s", createdSAId, createdAccessKeyId)
|
||||
})
|
||||
|
||||
t.Run("get_service_account", func(t *testing.T) {
|
||||
require.NotEmpty(t, createdSAId, "Service account should have been created")
|
||||
|
||||
resp, err := callIAMAPI(t, "GetServiceAccount", url.Values{
|
||||
"ServiceAccountId": {createdSAId},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
var getResp GetServiceAccountResponse
|
||||
err = xml.Unmarshal(body, &getResp)
|
||||
require.NoError(t, err)
|
||||
|
||||
sa := getResp.GetServiceAccountResult.ServiceAccount
|
||||
assert.Equal(t, createdSAId, sa.ServiceAccountId)
|
||||
assert.Equal(t, parentUserName, sa.ParentUser)
|
||||
assert.Empty(t, sa.SecretAccessKey, "SecretAccessKey should not be returned on Get")
|
||||
})
|
||||
|
||||
t.Run("list_service_accounts", func(t *testing.T) {
|
||||
resp, err := callIAMAPI(t, "ListServiceAccounts", url.Values{
|
||||
"ParentUser": {parentUserName},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
var listResp ListServiceAccountsResponse
|
||||
err = xml.Unmarshal(body, &listResp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.GreaterOrEqual(t, len(listResp.ListServiceAccountsResult.ServiceAccounts), 1,
|
||||
"Should have at least one service account for the parent user")
|
||||
})
|
||||
|
||||
t.Run("update_service_account_status", func(t *testing.T) {
|
||||
require.NotEmpty(t, createdSAId)
|
||||
|
||||
// Disable the service account
|
||||
resp, err := callIAMAPI(t, "UpdateServiceAccount", url.Values{
|
||||
"ServiceAccountId": {createdSAId},
|
||||
"Status": {"Inactive"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
// Verify it's now inactive
|
||||
getResp, err := callIAMAPI(t, "GetServiceAccount", url.Values{
|
||||
"ServiceAccountId": {createdSAId},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer getResp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(getResp.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
var result GetServiceAccountResponse
|
||||
err = xml.Unmarshal(body, &result)
|
||||
require.NoError(t, err, "Failed to parse response: %s", string(body))
|
||||
|
||||
assert.Equal(t, "Inactive", result.GetServiceAccountResult.ServiceAccount.Status)
|
||||
})
|
||||
|
||||
// Test that credentials could be used (verify they work with AWS SDK)
|
||||
// This must run BEFORE delete_service_account to use valid credentials
|
||||
t.Run("use_service_account_credentials", func(t *testing.T) {
|
||||
require.NotEmpty(t, createdAccessKeyId)
|
||||
require.NotEmpty(t, createdSecretAccessKey)
|
||||
|
||||
sess, err := session.NewSession(&aws.Config{
|
||||
Region: aws.String("us-east-1"),
|
||||
Endpoint: aws.String(TestIAMEndpoint), // IAM and S3 usually on same port in mini-seaweed
|
||||
Credentials: credentials.NewStaticCredentials(
|
||||
createdAccessKeyId,
|
||||
createdSecretAccessKey,
|
||||
"",
|
||||
),
|
||||
DisableSSL: aws.Bool(true),
|
||||
S3ForcePathStyle: aws.Bool(true),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
s3Client := s3.New(sess)
|
||||
_, err = s3Client.ListBuckets(&s3.ListBucketsInput{})
|
||||
|
||||
// Note: we don't necessarily expect success if no buckets/permissions
|
||||
// but we expect it not to fail with "InvalidAccessKeyId" or "SignatureDoesNotMatch"
|
||||
if err != nil {
|
||||
if aerr, ok := err.(awserr.Error); ok {
|
||||
assert.NotEqual(t, "InvalidAccessKeyId", aerr.Code(), "Credentials should be valid")
|
||||
assert.NotEqual(t, "SignatureDoesNotMatch", aerr.Code(), "Signature should be valid")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("delete_service_account", func(t *testing.T) {
|
||||
require.NotEmpty(t, createdSAId)
|
||||
|
||||
resp, err := callIAMAPI(t, "DeleteServiceAccount", url.Values{
|
||||
"ServiceAccountId": {createdSAId},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
// Verify it no longer exists
|
||||
getResp, err := callIAMAPI(t, "GetServiceAccount", url.Values{
|
||||
"ServiceAccountId": {createdSAId},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer getResp.Body.Close()
|
||||
|
||||
// Should return an error (not found)
|
||||
assert.NotEqual(t, http.StatusOK, getResp.StatusCode,
|
||||
"GetServiceAccount should fail after deletion")
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// TestServiceAccountValidation tests validation of service account operations
|
||||
func TestServiceAccountValidation(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test in short mode")
|
||||
}
|
||||
|
||||
if !isSeaweedFSRunning(t) {
|
||||
t.Skip("SeaweedFS is not running at", TestIAMEndpoint)
|
||||
}
|
||||
|
||||
t.Run("create_without_parent_user", func(t *testing.T) {
|
||||
resp, err := callIAMAPI(t, "CreateServiceAccount", url.Values{
|
||||
"Description": {"Test without parent"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.NotEqual(t, http.StatusOK, resp.StatusCode,
|
||||
"CreateServiceAccount without ParentUser should fail")
|
||||
})
|
||||
|
||||
t.Run("create_with_nonexistent_parent", func(t *testing.T) {
|
||||
resp, err := callIAMAPI(t, "CreateServiceAccount", url.Values{
|
||||
"ParentUser": {"nonexistent-user-12345"},
|
||||
"Description": {"Test with nonexistent parent"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.NotEqual(t, http.StatusOK, resp.StatusCode,
|
||||
"CreateServiceAccount with nonexistent parent should fail")
|
||||
})
|
||||
|
||||
t.Run("get_nonexistent_service_account", func(t *testing.T) {
|
||||
resp, err := callIAMAPI(t, "GetServiceAccount", url.Values{
|
||||
"ServiceAccountId": {"sa-NONEXISTENT123"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.NotEqual(t, http.StatusOK, resp.StatusCode,
|
||||
"GetServiceAccount for nonexistent ID should fail")
|
||||
})
|
||||
|
||||
t.Run("delete_nonexistent_service_account", func(t *testing.T) {
|
||||
resp, err := callIAMAPI(t, "DeleteServiceAccount", url.Values{
|
||||
"ServiceAccountId": {"sa-NONEXISTENT123"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.NotEqual(t, http.StatusOK, resp.StatusCode,
|
||||
"DeleteServiceAccount for nonexistent ID should fail")
|
||||
})
|
||||
}
|
||||
|
||||
// callIAMAPI is a helper to make IAM API calls
|
||||
func callIAMAPI(t *testing.T, action string, params url.Values) (*http.Response, error) {
|
||||
params.Set("Action", action)
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, TestIAMEndpoint+"/",
|
||||
strings.NewReader(params.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
return client.Do(req)
|
||||
}
|
||||
|
||||
// isSeaweedFSRunning checks if SeaweedFS S3 API is running
|
||||
func isSeaweedFSRunning(t *testing.T) bool {
|
||||
client := &http.Client{Timeout: 2 * time.Second}
|
||||
resp, err := client.Get(TestIAMEndpoint + "/status")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
return resp.StatusCode == http.StatusOK
|
||||
}
|
||||
@@ -1,260 +0,0 @@
|
||||
package iam
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// STS API test constants
|
||||
const (
|
||||
TestSTSEndpoint = "http://localhost:8333"
|
||||
)
|
||||
|
||||
// AssumeRoleWithWebIdentityResponse represents the STS response
|
||||
type AssumeRoleWithWebIdentityTestResponse struct {
|
||||
XMLName xml.Name `xml:"AssumeRoleWithWebIdentityResponse"`
|
||||
Result struct {
|
||||
Credentials struct {
|
||||
AccessKeyId string `xml:"AccessKeyId"`
|
||||
SecretAccessKey string `xml:"SecretAccessKey"`
|
||||
SessionToken string `xml:"SessionToken"`
|
||||
Expiration string `xml:"Expiration"`
|
||||
} `xml:"Credentials"`
|
||||
SubjectFromWebIdentityToken string `xml:"SubjectFromWebIdentityToken,omitempty"`
|
||||
} `xml:"AssumeRoleWithWebIdentityResult"`
|
||||
}
|
||||
|
||||
// STSErrorResponse represents an STS error response
|
||||
type STSErrorTestResponse struct {
|
||||
XMLName xml.Name `xml:"ErrorResponse"`
|
||||
Error struct {
|
||||
Type string `xml:"Type"`
|
||||
Code string `xml:"Code"`
|
||||
Message string `xml:"Message"`
|
||||
} `xml:"Error"`
|
||||
RequestId string `xml:"RequestId"`
|
||||
}
|
||||
|
||||
// TestAssumeRoleWithWebIdentityValidation tests input validation for the STS endpoint
|
||||
func TestAssumeRoleWithWebIdentityValidation(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test in short mode")
|
||||
}
|
||||
|
||||
if !isSTSEndpointRunning(t) {
|
||||
t.Skip("SeaweedFS STS endpoint is not running at", TestSTSEndpoint)
|
||||
}
|
||||
|
||||
t.Run("missing_web_identity_token", func(t *testing.T) {
|
||||
resp, err := callSTSAPI(t, url.Values{
|
||||
"Action": {"AssumeRoleWithWebIdentity"},
|
||||
"RoleArn": {"arn:aws:iam::role/test-role"},
|
||||
"RoleSessionName": {"test-session"},
|
||||
// WebIdentityToken is missing
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.NotEqual(t, http.StatusOK, resp.StatusCode,
|
||||
"Should fail without WebIdentityToken")
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
var errResp STSErrorTestResponse
|
||||
err = xml.Unmarshal(body, &errResp)
|
||||
require.NoError(t, err, "Failed to parse error response: %s", string(body))
|
||||
assert.Equal(t, "MissingParameter", errResp.Error.Code)
|
||||
})
|
||||
|
||||
t.Run("missing_role_arn", func(t *testing.T) {
|
||||
resp, err := callSTSAPI(t, url.Values{
|
||||
"Action": {"AssumeRoleWithWebIdentity"},
|
||||
"WebIdentityToken": {"fake-jwt-token"},
|
||||
"RoleSessionName": {"test-session"},
|
||||
// RoleArn is missing
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.NotEqual(t, http.StatusOK, resp.StatusCode,
|
||||
"Should fail without RoleArn")
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
var errResp STSErrorTestResponse
|
||||
err = xml.Unmarshal(body, &errResp)
|
||||
require.NoError(t, err, "Failed to parse error response: %s", string(body))
|
||||
assert.Equal(t, "MissingParameter", errResp.Error.Code)
|
||||
})
|
||||
|
||||
t.Run("missing_role_session_name", func(t *testing.T) {
|
||||
resp, err := callSTSAPI(t, url.Values{
|
||||
"Action": {"AssumeRoleWithWebIdentity"},
|
||||
"WebIdentityToken": {"fake-jwt-token"},
|
||||
"RoleArn": {"arn:aws:iam::role/test-role"},
|
||||
// RoleSessionName is missing
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.NotEqual(t, http.StatusOK, resp.StatusCode,
|
||||
"Should fail without RoleSessionName")
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
var errResp STSErrorTestResponse
|
||||
err = xml.Unmarshal(body, &errResp)
|
||||
require.NoError(t, err, "Failed to parse error response: %s", string(body))
|
||||
assert.Equal(t, "MissingParameter", errResp.Error.Code)
|
||||
})
|
||||
|
||||
t.Run("invalid_jwt_token", func(t *testing.T) {
|
||||
resp, err := callSTSAPI(t, url.Values{
|
||||
"Action": {"AssumeRoleWithWebIdentity"},
|
||||
"WebIdentityToken": {"not-a-valid-jwt-token"},
|
||||
"RoleArn": {"arn:aws:iam::role/test-role"},
|
||||
"RoleSessionName": {"test-session"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Should fail with AccessDenied since the JWT is invalid
|
||||
assert.NotEqual(t, http.StatusOK, resp.StatusCode,
|
||||
"Should fail with invalid JWT token")
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
var errResp STSErrorTestResponse
|
||||
err = xml.Unmarshal(body, &errResp)
|
||||
require.NoError(t, err, "Failed to parse error response: %s", string(body))
|
||||
assert.Contains(t, []string{"AccessDenied", "InvalidParameterValue"}, errResp.Error.Code)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAssumeRoleWithWebIdentityWithMockJWT tests the STS endpoint with mock JWTs
|
||||
// This test requires the mock OIDC provider to be configured
|
||||
func TestAssumeRoleWithWebIdentityWithMockJWT(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test in short mode")
|
||||
}
|
||||
|
||||
if !isSTSEndpointRunning(t) {
|
||||
t.Skip("SeaweedFS STS endpoint is not running at", TestSTSEndpoint)
|
||||
}
|
||||
|
||||
// Create a test framework to get valid JWT tokens
|
||||
framework := NewS3IAMTestFramework(t)
|
||||
defer framework.Cleanup()
|
||||
|
||||
// Generate a test JWT using the framework
|
||||
testUsername := "sts-test-user"
|
||||
testRole := "readonly"
|
||||
|
||||
// Try to get a token - use Keycloak if available, otherwise generate a mock JWT
|
||||
var token string
|
||||
var err error
|
||||
if framework.useKeycloak {
|
||||
token, err = framework.getKeycloakToken(testUsername)
|
||||
} else {
|
||||
// Generate a mock JWT token with 1 hour validity
|
||||
token, err = framework.generateJWTToken(testUsername, testRole, time.Hour)
|
||||
}
|
||||
if err != nil {
|
||||
t.Skipf("Unable to generate test JWT (requires mock OIDC or Keycloak): %v", err)
|
||||
}
|
||||
|
||||
t.Run("valid_jwt_token", func(t *testing.T) {
|
||||
resp, err := callSTSAPI(t, url.Values{
|
||||
"Action": {"AssumeRoleWithWebIdentity"},
|
||||
"WebIdentityToken": {token},
|
||||
"RoleArn": {"arn:aws:iam::role/" + testRole},
|
||||
"RoleSessionName": {"integration-test-session"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
t.Logf("Response status: %d, body: %s", resp.StatusCode, string(body))
|
||||
|
||||
// Note: This may still fail if the role/trust policy is not configured
|
||||
// In that case, we just verify the error is about trust policy, not token validation
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
var errResp STSErrorTestResponse
|
||||
err = xml.Unmarshal(body, &errResp)
|
||||
require.NoError(t, err, "Failed to parse error response: %s", string(body))
|
||||
assert.NotEqual(t, "InvalidParameterValue", errResp.Error.Code,
|
||||
"Token validation should not fail - error should be about trust policy")
|
||||
} else {
|
||||
var stsResp AssumeRoleWithWebIdentityTestResponse
|
||||
err = xml.Unmarshal(body, &stsResp)
|
||||
require.NoError(t, err, "Failed to parse response: %s", string(body))
|
||||
|
||||
creds := stsResp.Result.Credentials
|
||||
assert.NotEmpty(t, creds.AccessKeyId, "AccessKeyId should not be empty")
|
||||
assert.NotEmpty(t, creds.SecretAccessKey, "SecretAccessKey should not be empty")
|
||||
assert.NotEmpty(t, creds.SessionToken, "SessionToken should not be empty")
|
||||
assert.NotEmpty(t, creds.Expiration, "Expiration should not be empty")
|
||||
|
||||
t.Logf("Successfully obtained temporary credentials: AccessKeyId=%s", creds.AccessKeyId)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with_duration_seconds", func(t *testing.T) {
|
||||
resp, err := callSTSAPI(t, url.Values{
|
||||
"Action": {"AssumeRoleWithWebIdentity"},
|
||||
"WebIdentityToken": {token},
|
||||
"RoleArn": {"arn:aws:iam::role/" + testRole},
|
||||
"RoleSessionName": {"integration-test-session"},
|
||||
"DurationSeconds": {"3600"}, // 1 hour
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Verify the request is accepted (even if trust policy causes rejection)
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should not fail with InvalidParameterValue for DurationSeconds
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
var errResp STSErrorTestResponse
|
||||
err = xml.Unmarshal(body, &errResp)
|
||||
require.NoError(t, err, "Failed to parse error response: %s", string(body))
|
||||
assert.NotContains(t, errResp.Error.Message, "DurationSeconds",
|
||||
"DurationSeconds parameter should be accepted")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// callSTSAPI is a helper to make STS API calls
|
||||
func callSTSAPI(t *testing.T, params url.Values) (*http.Response, error) {
|
||||
req, err := http.NewRequest(http.MethodPost, TestSTSEndpoint+"/",
|
||||
strings.NewReader(params.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
return client.Do(req)
|
||||
}
|
||||
|
||||
// isSTSEndpointRunning checks if SeaweedFS STS endpoint is running
|
||||
func isSTSEndpointRunning(t *testing.T) bool {
|
||||
client := &http.Client{Timeout: 2 * time.Second}
|
||||
resp, err := client.Get(TestSTSEndpoint + "/status")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
return resp.StatusCode == http.StatusOK
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
# Cross-Filesystem Compatibility Test Results
|
||||
|
||||
## Overview
|
||||
|
||||
This document summarizes the cross-filesystem compatibility testing between **s3fs** and **PyArrow native S3 filesystem** implementations when working with SeaweedFS.
|
||||
|
||||
## Test Purpose
|
||||
|
||||
Verify that Parquet files written using one filesystem implementation (s3fs or PyArrow native S3) can be correctly read using the other implementation, confirming true file format compatibility.
|
||||
|
||||
## Test Methodology
|
||||
|
||||
### Test Matrix
|
||||
|
||||
The test performs the following combinations:
|
||||
|
||||
1. **Write with s3fs → Read with PyArrow native S3**
|
||||
2. **Write with PyArrow native S3 → Read with s3fs**
|
||||
|
||||
For each direction, the test:
|
||||
- Creates a sample PyArrow table with multiple data types (int64, string, float64, bool)
|
||||
- Writes the Parquet file using one filesystem implementation
|
||||
- Reads the Parquet file using the other filesystem implementation
|
||||
- Verifies data integrity by comparing:
|
||||
- Row counts
|
||||
- Schema equality
|
||||
- Data contents (after sorting by ID to handle row order differences)
|
||||
|
||||
### File Sizes Tested
|
||||
|
||||
- **Small files**: 5 rows (quick validation)
|
||||
- **Large files**: 200,000 rows (multi-row-group validation)
|
||||
|
||||
## Test Results
|
||||
|
||||
### ✅ Small Files (5 rows)
|
||||
|
||||
| Write Method | Read Method | Result | Read Function Used |
|
||||
|--------------|-------------|--------|--------------------|
|
||||
| s3fs | PyArrow native S3 | ✅ PASS | pq.read_table |
|
||||
| PyArrow native S3 | s3fs | ✅ PASS | pq.read_table |
|
||||
|
||||
**Status**: **ALL TESTS PASSED**
|
||||
|
||||
### Large Files (200,000 rows)
|
||||
|
||||
Large file testing requires adequate volume capacity in SeaweedFS. When run with default volume settings (50MB max size), tests may encounter capacity issues with the number of large test files created simultaneously.
|
||||
|
||||
**Recommendation**: For large file testing, increase `VOLUME_MAX_SIZE_MB` in the Makefile or run tests with `TEST_QUICK=1` for development/validation purposes.
|
||||
|
||||
## Key Findings
|
||||
|
||||
### ✅ Full Compatibility Confirmed
|
||||
|
||||
**Files written with s3fs and PyArrow native S3 filesystem are fully compatible and can be read by either implementation.**
|
||||
|
||||
This confirms that:
|
||||
|
||||
1. **Identical Parquet Format**: Both s3fs and PyArrow native S3 use the same underlying PyArrow library to generate Parquet files, resulting in identical file formats at the binary level.
|
||||
|
||||
2. **S3 API Compatibility**: SeaweedFS's S3 implementation handles both filesystem backends correctly, with proper:
|
||||
- Object creation (PutObject)
|
||||
- Object reading (GetObject)
|
||||
- Directory handling (implicit directories)
|
||||
- Multipart uploads (for larger files)
|
||||
|
||||
3. **Metadata Consistency**: File metadata, schemas, and data integrity are preserved across both write and read operations regardless of which filesystem implementation is used.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Common Write Path
|
||||
|
||||
Both implementations use PyArrow's `pads.write_dataset()` function:
|
||||
|
||||
```python
|
||||
# s3fs approach
|
||||
fs = s3fs.S3FileSystem(...)
|
||||
pads.write_dataset(table, path, format="parquet", filesystem=fs)
|
||||
|
||||
# PyArrow native approach
|
||||
s3 = pafs.S3FileSystem(...)
|
||||
pads.write_dataset(table, path, format="parquet", filesystem=s3)
|
||||
```
|
||||
|
||||
### Multiple Read Methods Tested
|
||||
|
||||
The test attempts reads using multiple PyArrow methods:
|
||||
- `pq.read_table()` - Direct table reading
|
||||
- `pq.ParquetDataset()` - Dataset-based reading
|
||||
- `pads.dataset()` - PyArrow dataset API
|
||||
|
||||
All methods successfully read files written by either filesystem implementation.
|
||||
|
||||
## Practical Implications
|
||||
|
||||
### For Users
|
||||
|
||||
1. **Flexibility**: Users can choose either s3fs or PyArrow native S3 based on their preferences:
|
||||
- **s3fs**: More mature, widely used, familiar API
|
||||
- **PyArrow native**: Pure PyArrow solution, fewer dependencies
|
||||
|
||||
2. **Interoperability**: Teams using different tools can seamlessly share Parquet datasets stored in SeaweedFS
|
||||
|
||||
3. **Migration**: Easy to migrate between filesystem implementations without data conversion
|
||||
|
||||
### For SeaweedFS
|
||||
|
||||
1. **S3 Compatibility**: Confirms SeaweedFS's S3 implementation is compatible with major Python data science tools
|
||||
|
||||
2. **Implicit Directory Handling**: The implicit directory fix works correctly for both filesystem implementations
|
||||
|
||||
3. **Standard Compliance**: SeaweedFS handles S3 operations in a way that's compatible with AWS S3 behavior
|
||||
|
||||
## Running the Tests
|
||||
|
||||
### Quick Test (Recommended for Development)
|
||||
|
||||
```bash
|
||||
cd test/s3/parquet
|
||||
TEST_QUICK=1 make test-cross-fs-with-server
|
||||
```
|
||||
|
||||
### Full Test (All File Sizes)
|
||||
|
||||
```bash
|
||||
cd test/s3/parquet
|
||||
make test-cross-fs-with-server
|
||||
```
|
||||
|
||||
### Manual Test (Assuming Server is Running)
|
||||
|
||||
```bash
|
||||
cd test/s3/parquet
|
||||
make setup-python
|
||||
make start-seaweedfs-ci
|
||||
|
||||
# In another terminal
|
||||
TEST_QUICK=1 make test-cross-fs
|
||||
|
||||
# Cleanup
|
||||
make stop-seaweedfs-safe
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
The test supports customization through environment variables:
|
||||
|
||||
- `S3_ENDPOINT_URL`: S3 endpoint (default: `http://localhost:8333`)
|
||||
- `S3_ACCESS_KEY`: Access key (default: `some_access_key1`)
|
||||
- `S3_SECRET_KEY`: Secret key (default: `some_secret_key1`)
|
||||
- `BUCKET_NAME`: Bucket name (default: `test-parquet-bucket`)
|
||||
- `TEST_QUICK`: Run only small tests (default: `0`, set to `1` for quick mode)
|
||||
|
||||
## Conclusion
|
||||
|
||||
The cross-filesystem compatibility tests demonstrate that **Parquet files written via s3fs and PyArrow native S3 filesystem are completely interchangeable**. This validates that:
|
||||
|
||||
1. The Parquet file format is implementation-agnostic
|
||||
2. SeaweedFS's S3 API correctly handles both filesystem backends
|
||||
3. Users have full flexibility in choosing their preferred filesystem implementation
|
||||
|
||||
This compatibility is a testament to:
|
||||
- PyArrow's consistent file format generation
|
||||
- SeaweedFS's robust S3 API implementation
|
||||
- Proper handling of S3 semantics (especially implicit directories)
|
||||
|
||||
---
|
||||
|
||||
**Test Implementation**: `test_cross_filesystem_compatibility.py`
|
||||
**Last Updated**: November 21, 2024
|
||||
**Status**: ✅ All critical tests passing
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# Final Root Cause Analysis
|
||||
|
||||
## Overview
|
||||
|
||||
This document provides a deep technical analysis of the s3fs compatibility issue with PyArrow Parquet datasets on SeaweedFS, and the solution implemented to resolve it.
|
||||
|
||||
## Root Cause
|
||||
|
||||
When PyArrow writes datasets using `write_dataset()`, it creates implicit directory structures by writing files without explicit directory markers. However, some S3 workflows may create 0-byte directory markers.
|
||||
|
||||
### The Problem
|
||||
|
||||
1. **PyArrow writes dataset files** without creating explicit directory objects
|
||||
2. **s3fs calls HEAD** on the directory path to check if it exists
|
||||
3. **If HEAD returns 200** with `Content-Length: 0`, s3fs interprets it as a file (not a directory)
|
||||
4. **PyArrow fails** when trying to read, reporting "Parquet file size is 0 bytes"
|
||||
|
||||
### AWS S3 Behavior
|
||||
|
||||
AWS S3 returns **404 Not Found** for implicit directories (directories that only exist because they have children but no explicit marker object). This allows s3fs to fall back to LIST operations to detect the directory.
|
||||
|
||||
## The Solution
|
||||
|
||||
### Implementation
|
||||
|
||||
Modified the S3 API HEAD handler in `weed/s3api/s3api_object_handlers.go` to:
|
||||
|
||||
1. **Check if object ends with `/`**: Explicit directory markers return 200 as before
|
||||
2. **Check if object has children**: If a 0-byte object has children in the filer, treat it as an implicit directory
|
||||
3. **Return 404 for implicit directories**: This matches AWS S3 behavior and triggers s3fs's LIST fallback
|
||||
|
||||
### Code Changes
|
||||
|
||||
The fix is implemented in the `HeadObjectHandler` function with logic to:
|
||||
- Detect implicit directories by checking for child entries
|
||||
- Return 404 (NoSuchKey) for implicit directories
|
||||
- Preserve existing behavior for explicit directory markers and regular files
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Optimization: Child Check Cache
|
||||
- Child existence checks are performed via filer LIST operations
|
||||
- Results could be cached for frequently accessed paths
|
||||
- Trade-off between consistency and performance
|
||||
|
||||
### Impact
|
||||
- Minimal performance impact for normal file operations
|
||||
- Slight overhead for HEAD requests on implicit directories (one additional LIST call)
|
||||
- Overall improvement in PyArrow compatibility outweighs minor performance cost
|
||||
|
||||
## TODO
|
||||
|
||||
- [ ] Add detailed benchmarking results comparing before/after fix
|
||||
- [ ] Document edge cases discovered during implementation
|
||||
- [ ] Add architectural diagrams showing the request flow
|
||||
- [ ] Document alternative solutions considered and why they were rejected
|
||||
- [ ] Add performance profiling data for child existence checks
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
# MinIO Directory Handling Comparison
|
||||
|
||||
## Overview
|
||||
|
||||
This document compares how MinIO handles directory markers versus SeaweedFS's implementation, and explains the different approaches to S3 directory semantics.
|
||||
|
||||
## MinIO's Approach
|
||||
|
||||
MinIO handles implicit directories similarly to AWS S3:
|
||||
|
||||
1. **No explicit directory objects**: Directories are implicit, defined only by object key prefixes
|
||||
2. **HEAD on directory returns 404**: Consistent with AWS S3 behavior
|
||||
3. **LIST operations reveal directories**: Directories are discovered through delimiter-based LIST operations
|
||||
4. **Automatic prefix handling**: MinIO automatically recognizes prefixes as directories
|
||||
|
||||
### MinIO Implementation Details
|
||||
|
||||
- Uses in-memory metadata for fast prefix lookups
|
||||
- Optimized for LIST operations with common delimiter (`/`)
|
||||
- No persistent directory objects in storage layer
|
||||
- Directories "exist" as long as they contain objects
|
||||
|
||||
## SeaweedFS Approach
|
||||
|
||||
SeaweedFS uses a filer-based approach with real directory entries:
|
||||
|
||||
### Before the Fix
|
||||
|
||||
1. **Explicit directory objects**: Could create 0-byte objects as directory markers
|
||||
2. **HEAD returns 200**: Even for implicit directories
|
||||
3. **Caused s3fs issues**: s3fs interpreted 0-byte HEAD responses as empty files
|
||||
|
||||
### After the Fix
|
||||
|
||||
1. **Hybrid approach**: Supports both explicit markers (with `/` suffix) and implicit directories
|
||||
2. **HEAD returns 404 for implicit directories**: Matches AWS S3 and MinIO behavior
|
||||
3. **Filer integration**: Uses filer's directory metadata to detect implicit directories
|
||||
4. **s3fs compatibility**: Triggers proper LIST fallback behavior
|
||||
|
||||
## Key Differences
|
||||
|
||||
| Aspect | MinIO | SeaweedFS (After Fix) |
|
||||
|--------|-------|----------------------|
|
||||
| Directory Storage | No persistent objects | Filer directory entries |
|
||||
| Implicit Directory HEAD | 404 Not Found | 404 Not Found |
|
||||
| Explicit Marker HEAD | Not applicable | 200 OK (with `/` suffix) |
|
||||
| Child Detection | Prefix scan | Filer LIST operation |
|
||||
| Performance | In-memory lookups | Filer gRPC calls |
|
||||
|
||||
## Implementation Considerations
|
||||
|
||||
### Advantages of SeaweedFS Approach
|
||||
- Integrates with existing filer metadata
|
||||
- Supports both implicit and explicit directories
|
||||
- Preserves directory metadata and attributes
|
||||
- Compatible with POSIX filer semantics
|
||||
|
||||
### Trade-offs
|
||||
- Additional filer communication overhead for HEAD requests
|
||||
- Complexity of supporting both directory paradigms
|
||||
- Performance depends on filer efficiency
|
||||
|
||||
## TODO
|
||||
|
||||
- [ ] Add performance benchmark comparison: MinIO vs SeaweedFS
|
||||
- [ ] Document edge cases where behaviors differ
|
||||
- [ ] Add example request/response traces for both systems
|
||||
- [ ] Document migration path for users moving from MinIO to SeaweedFS
|
||||
- [ ] Add compatibility matrix for different S3 clients
|
||||
|
||||
+128
-36
@@ -4,9 +4,14 @@
|
||||
# Default values
|
||||
SEAWEEDFS_BINARY ?= weed
|
||||
S3_PORT ?= 8333
|
||||
FILER_PORT ?= 8888
|
||||
VOLUME_PORT ?= 8080
|
||||
MASTER_PORT ?= 9333
|
||||
TEST_TIMEOUT ?= 15m
|
||||
ACCESS_KEY ?= some_access_key1
|
||||
SECRET_KEY ?= some_secret_key1
|
||||
VOLUME_MAX_SIZE_MB ?= 50
|
||||
VOLUME_MAX_COUNT ?= 100
|
||||
BUCKET_NAME ?= test-parquet-bucket
|
||||
ENABLE_SSE_S3 ?= false
|
||||
|
||||
@@ -63,7 +68,11 @@ help:
|
||||
@echo "Configuration:"
|
||||
@echo " SEAWEEDFS_BINARY=$(SEAWEEDFS_BINARY)"
|
||||
@echo " S3_PORT=$(S3_PORT)"
|
||||
@echo " FILER_PORT=$(FILER_PORT)"
|
||||
@echo " VOLUME_PORT=$(VOLUME_PORT)"
|
||||
@echo " MASTER_PORT=$(MASTER_PORT)"
|
||||
@echo " BUCKET_NAME=$(BUCKET_NAME)"
|
||||
@echo " VOLUME_MAX_SIZE_MB=$(VOLUME_MAX_SIZE_MB)"
|
||||
@echo " ENABLE_SSE_S3=$(ENABLE_SSE_S3)"
|
||||
@echo " PYTHON=$(PYTHON)"
|
||||
|
||||
@@ -97,25 +106,39 @@ setup-python: check-python
|
||||
start-seaweedfs-ci: check-binary
|
||||
@echo "$(YELLOW)Starting SeaweedFS server for Parquet testing...$(NC)"
|
||||
|
||||
# Clean up any existing processes first (CI-safe) - aggressive cleanup
|
||||
@echo "Aggressively cleaning up any existing processes on S3 port $(S3_PORT) and master port 9333..."
|
||||
# Clean up any existing processes first (CI-safe)
|
||||
@echo "Cleaning up any existing processes..."
|
||||
@if command -v lsof >/dev/null 2>&1; then \
|
||||
for attempt in 1 2 3; do \
|
||||
lsof -ti :$(S3_PORT) 2>/dev/null | head -5 | while read pid; do kill -9 $$pid 2>/dev/null || true; done; \
|
||||
lsof -ti :9333 2>/dev/null | head -5 | while read pid; do kill -9 $$pid 2>/dev/null || true; done; \
|
||||
sleep 1; \
|
||||
done; \
|
||||
lsof -ti :$(MASTER_PORT) 2>/dev/null | head -5 | while read pid; do kill -TERM $$pid 2>/dev/null || true; done; \
|
||||
lsof -ti :$(VOLUME_PORT) 2>/dev/null | head -5 | while read pid; do kill -TERM $$pid 2>/dev/null || true; done; \
|
||||
lsof -ti :$(FILER_PORT) 2>/dev/null | head -5 | while read pid; do kill -TERM $$pid 2>/dev/null || true; done; \
|
||||
lsof -ti :$(S3_PORT) 2>/dev/null | head -5 | while read pid; do kill -TERM $$pid 2>/dev/null || true; done; \
|
||||
lsof -ti :$$(( $(MASTER_PORT) + 10000 )) 2>/dev/null | head -5 | while read pid; do kill -TERM $$pid 2>/dev/null || true; done; \
|
||||
lsof -ti :$$(( $(VOLUME_PORT) + 10000 )) 2>/dev/null | head -5 | while read pid; do kill -TERM $$pid 2>/dev/null || true; done; \
|
||||
lsof -ti :$$(( $(FILER_PORT) + 10000 )) 2>/dev/null | head -5 | while read pid; do kill -TERM $$pid 2>/dev/null || true; done; \
|
||||
fi
|
||||
@sleep 3
|
||||
@sleep 2
|
||||
|
||||
# Create necessary directories
|
||||
@mkdir -p /tmp/seaweedfs-test-parquet
|
||||
@mkdir -p /tmp/seaweedfs-test-parquet-master
|
||||
@mkdir -p /tmp/seaweedfs-test-parquet-volume
|
||||
@mkdir -p /tmp/seaweedfs-test-parquet-filer
|
||||
|
||||
# Clean up any old server logs
|
||||
@rm -f /tmp/seaweedfs-parquet-*.log || true
|
||||
|
||||
# Start weed mini with embedded S3
|
||||
@echo "Starting weed mini with embedded S3..."
|
||||
# Start master server with volume size limit and explicit gRPC port
|
||||
@echo "Starting master server..."
|
||||
@nohup $(SEAWEEDFS_BINARY) master -port=$(MASTER_PORT) -port.grpc=$$(( $(MASTER_PORT) + 10000 )) -mdir=/tmp/seaweedfs-test-parquet-master -volumeSizeLimitMB=$(VOLUME_MAX_SIZE_MB) -ip=127.0.0.1 -peers=none > /tmp/seaweedfs-parquet-master.log 2>&1 &
|
||||
@sleep 3
|
||||
|
||||
# Start volume server with master HTTP port and increased capacity
|
||||
@echo "Starting volume server..."
|
||||
@nohup $(SEAWEEDFS_BINARY) volume -port=$(VOLUME_PORT) -master=127.0.0.1:$(MASTER_PORT) -dir=/tmp/seaweedfs-test-parquet-volume -max=$(VOLUME_MAX_COUNT) -ip=127.0.0.1 -preStopSeconds=1 > /tmp/seaweedfs-parquet-volume.log 2>&1 &
|
||||
@sleep 5
|
||||
|
||||
# Start filer server with embedded S3
|
||||
@echo "Starting filer server with embedded S3..."
|
||||
@if [ "$(ENABLE_SSE_S3)" = "true" ]; then \
|
||||
echo " SSE-S3 encryption: ENABLED"; \
|
||||
printf '{"identities":[{"name":"%s","credentials":[{"accessKey":"%s","secretKey":"%s"}],"actions":["Admin","Read","Write"]}],"buckets":[{"name":"$(BUCKET_NAME)","encryption":{"sseS3":{"enabled":true}}}]}' "$(ACCESS_KEY)" "$(ACCESS_KEY)" "$(SECRET_KEY)" > /tmp/seaweedfs-parquet-s3.json; \
|
||||
@@ -123,43 +146,96 @@ start-seaweedfs-ci: check-binary
|
||||
echo " SSE-S3 encryption: DISABLED"; \
|
||||
printf '{"identities":[{"name":"%s","credentials":[{"accessKey":"%s","secretKey":"%s"}],"actions":["Admin","Read","Write"]}]}' "$(ACCESS_KEY)" "$(ACCESS_KEY)" "$(SECRET_KEY)" > /tmp/seaweedfs-parquet-s3.json; \
|
||||
fi
|
||||
@$(SEAWEEDFS_BINARY) mini \
|
||||
-dir=/tmp/seaweedfs-test-parquet \
|
||||
-ip.bind=0.0.0.0 \
|
||||
-s3.port=$(S3_PORT) \
|
||||
-s3.config=/tmp/seaweedfs-parquet-s3.json \
|
||||
> /tmp/seaweedfs-parquet-mini.log 2>&1 & echo $$! > /tmp/weed-mini.pid
|
||||
@echo "Waiting for S3 service to be fully ready (max 90 seconds)..."
|
||||
@bash -c 'for i in $$(seq 1 90); do \
|
||||
if curl -s -H "Authorization: AWS4-HMAC-SHA256 Credential=$(ACCESS_KEY)" http://localhost:$(S3_PORT)/ > /dev/null 2>&1; then \
|
||||
echo "✅ S3 service is ready"; \
|
||||
sleep 2; \
|
||||
exit 0; \
|
||||
@AWS_ACCESS_KEY_ID=$(ACCESS_KEY) AWS_SECRET_ACCESS_KEY=$(SECRET_KEY) nohup $(SEAWEEDFS_BINARY) filer -port=$(FILER_PORT) -port.grpc=$$(( $(FILER_PORT) + 10000 )) -master=127.0.0.1:$(MASTER_PORT) -dataCenter=defaultDataCenter -ip=127.0.0.1 -s3 -s3.port=$(S3_PORT) -s3.config=/tmp/seaweedfs-parquet-s3.json > /tmp/seaweedfs-parquet-filer.log 2>&1 &
|
||||
@sleep 5
|
||||
|
||||
# Wait for S3 service to be ready - use port-based checking for reliability
|
||||
@echo "$(YELLOW)Waiting for S3 service to be ready...$(NC)"
|
||||
@for i in $$(seq 1 20); do \
|
||||
if netstat -an 2>/dev/null | grep -q ":$(S3_PORT).*LISTEN" || \
|
||||
ss -an 2>/dev/null | grep -q ":$(S3_PORT).*LISTEN" || \
|
||||
lsof -i :$(S3_PORT) >/dev/null 2>&1; then \
|
||||
echo "$(GREEN)S3 service is listening on port $(S3_PORT)$(NC)"; \
|
||||
sleep 1; \
|
||||
break; \
|
||||
fi; \
|
||||
if [ $$i -eq 20 ]; then \
|
||||
echo "$(RED)S3 service failed to start within 20 seconds$(NC)"; \
|
||||
echo "=== Detailed Logs ==="; \
|
||||
echo "Master log:"; tail -30 /tmp/seaweedfs-parquet-master.log || true; \
|
||||
echo "Volume log:"; tail -30 /tmp/seaweedfs-parquet-volume.log || true; \
|
||||
echo "Filer log:"; tail -30 /tmp/seaweedfs-parquet-filer.log || true; \
|
||||
echo "=== Port Status ==="; \
|
||||
netstat -an 2>/dev/null | grep ":$(S3_PORT)" || \
|
||||
ss -an 2>/dev/null | grep ":$(S3_PORT)" || \
|
||||
echo "No port listening on $(S3_PORT)"; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
echo "Waiting for S3 service... ($$i/20)"; \
|
||||
sleep 1; \
|
||||
done; \
|
||||
echo "ERROR S3 service failed to start within 90 seconds"; \
|
||||
echo "=== Server log output ==="; \
|
||||
cat /tmp/seaweedfs-parquet-mini.log 2>/dev/null || echo "No startup log available"; \
|
||||
exit 1'
|
||||
done
|
||||
|
||||
# Additional wait for filer gRPC to be ready
|
||||
@echo "$(YELLOW)Waiting for filer gRPC to be ready...$(NC)"
|
||||
@sleep 2
|
||||
|
||||
# Wait for volume server to register with master and ensure volume assignment works
|
||||
@echo "$(YELLOW)Waiting for volume assignment to be ready...$(NC)"
|
||||
@for i in $$(seq 1 30); do \
|
||||
ASSIGN_RESULT=$$(curl -s "http://localhost:$(MASTER_PORT)/dir/assign?count=1" 2>/dev/null); \
|
||||
if echo "$$ASSIGN_RESULT" | grep -q '"fid"'; then \
|
||||
echo "$(GREEN)Volume assignment is ready$(NC)"; \
|
||||
break; \
|
||||
fi; \
|
||||
if [ $$i -eq 30 ]; then \
|
||||
echo "$(RED)Volume assignment not ready after 30 seconds$(NC)"; \
|
||||
echo "=== Last assign attempt ==="; \
|
||||
echo "$$ASSIGN_RESULT"; \
|
||||
echo "=== Master Status ==="; \
|
||||
curl -s "http://localhost:$(MASTER_PORT)/dir/status" 2>/dev/null || echo "Failed to get master status"; \
|
||||
echo "=== Master Logs ==="; \
|
||||
tail -50 /tmp/seaweedfs-parquet-master.log 2>/dev/null || echo "No master log"; \
|
||||
echo "=== Volume Logs ==="; \
|
||||
tail -50 /tmp/seaweedfs-parquet-volume.log 2>/dev/null || echo "No volume log"; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
echo "Waiting for volume assignment... ($$i/30)"; \
|
||||
sleep 1; \
|
||||
done
|
||||
|
||||
@echo "$(GREEN)SeaweedFS server started successfully for Parquet testing$(NC)"
|
||||
@echo "Master: http://localhost:$(MASTER_PORT)"
|
||||
@echo "Volume: http://localhost:$(VOLUME_PORT)"
|
||||
@echo "Filer: http://localhost:$(FILER_PORT)"
|
||||
@echo "S3: http://localhost:$(S3_PORT)"
|
||||
@echo "Volume Max Size: $(VOLUME_MAX_SIZE_MB)MB"
|
||||
|
||||
start-seaweedfs: check-binary
|
||||
@echo "$(YELLOW)Starting SeaweedFS server for Parquet testing...$(NC)"
|
||||
@# Use port-based cleanup for consistency and safety
|
||||
@echo "Cleaning up any existing processes..."
|
||||
@lsof -ti :$(MASTER_PORT) 2>/dev/null | xargs -r kill -TERM || true
|
||||
@lsof -ti :$(VOLUME_PORT) 2>/dev/null | xargs -r kill -TERM || true
|
||||
@lsof -ti :$(FILER_PORT) 2>/dev/null | xargs -r kill -TERM || true
|
||||
@lsof -ti :$(S3_PORT) 2>/dev/null | xargs -r kill -TERM || true
|
||||
@# Clean up gRPC ports (HTTP port + 10000)
|
||||
@lsof -ti :$$(( $(MASTER_PORT) + 10000 )) 2>/dev/null | xargs -r kill -TERM || true
|
||||
@lsof -ti :$$(( $(VOLUME_PORT) + 10000 )) 2>/dev/null | xargs -r kill -TERM || true
|
||||
@lsof -ti :$$(( $(FILER_PORT) + 10000 )) 2>/dev/null | xargs -r kill -TERM || true
|
||||
@sleep 2
|
||||
@$(MAKE) start-seaweedfs-ci
|
||||
|
||||
stop-seaweedfs:
|
||||
@echo "$(YELLOW)Stopping SeaweedFS server...$(NC)"
|
||||
@# Use port-based cleanup for consistency and safety
|
||||
@if [ -f /tmp/weed-mini.pid ]; then \
|
||||
echo "Stopping weed mini..."; \
|
||||
kill $$(cat /tmp/weed-mini.pid) || true; \
|
||||
rm -f /tmp/weed-mini.pid; \
|
||||
fi
|
||||
@lsof -ti :$(MASTER_PORT) 2>/dev/null | xargs -r kill -TERM || true
|
||||
@lsof -ti :$(VOLUME_PORT) 2>/dev/null | xargs -r kill -TERM || true
|
||||
@lsof -ti :$(FILER_PORT) 2>/dev/null | xargs -r kill -TERM || true
|
||||
@lsof -ti :$(S3_PORT) 2>/dev/null | xargs -r kill -TERM || true
|
||||
@# Clean up gRPC ports (HTTP port + 10000)
|
||||
@lsof -ti :$$(( $(MASTER_PORT) + 10000 )) 2>/dev/null | xargs -r kill -TERM || true
|
||||
@lsof -ti :$$(( $(VOLUME_PORT) + 10000 )) 2>/dev/null | xargs -r kill -TERM || true
|
||||
@lsof -ti :$$(( $(FILER_PORT) + 10000 )) 2>/dev/null | xargs -r kill -TERM || true
|
||||
@sleep 2
|
||||
@echo "$(GREEN)SeaweedFS server stopped$(NC)"
|
||||
|
||||
@@ -169,10 +245,22 @@ stop-seaweedfs-safe:
|
||||
@# Use port-based cleanup which is safer in CI
|
||||
@if command -v lsof >/dev/null 2>&1; then \
|
||||
echo "Using lsof for port-based cleanup..."; \
|
||||
lsof -ti :$(MASTER_PORT) 2>/dev/null | head -5 | while read pid; do kill -TERM $$pid 2>/dev/null || true; done; \
|
||||
lsof -ti :$(VOLUME_PORT) 2>/dev/null | head -5 | while read pid; do kill -TERM $$pid 2>/dev/null || true; done; \
|
||||
lsof -ti :$(FILER_PORT) 2>/dev/null | head -5 | while read pid; do kill -TERM $$pid 2>/dev/null || true; done; \
|
||||
lsof -ti :$(S3_PORT) 2>/dev/null | head -5 | while read pid; do kill -TERM $$pid 2>/dev/null || true; done; \
|
||||
lsof -ti :$$(( $(MASTER_PORT) + 10000 )) 2>/dev/null | head -5 | while read pid; do kill -TERM $$pid 2>/dev/null || true; done; \
|
||||
lsof -ti :$$(( $(VOLUME_PORT) + 10000 )) 2>/dev/null | head -5 | while read pid; do kill -TERM $$pid 2>/dev/null || true; done; \
|
||||
lsof -ti :$$(( $(FILER_PORT) + 10000 )) 2>/dev/null | head -5 | while read pid; do kill -TERM $$pid 2>/dev/null || true; done; \
|
||||
else \
|
||||
echo "lsof not available, using netstat approach..."; \
|
||||
netstat -tlnp 2>/dev/null | grep :$(MASTER_PORT) | awk '{print $$7}' | cut -d/ -f1 | head -5 | while read pid; do [ "$$pid" != "-" ] && kill -TERM $$pid 2>/dev/null || true; done; \
|
||||
netstat -tlnp 2>/dev/null | grep :$(VOLUME_PORT) | awk '{print $$7}' | cut -d/ -f1 | head -5 | while read pid; do [ "$$pid" != "-" ] && kill -TERM $$pid 2>/dev/null || true; done; \
|
||||
netstat -tlnp 2>/dev/null | grep :$(FILER_PORT) | awk '{print $$7}' | cut -d/ -f1 | head -5 | while read pid; do [ "$$pid" != "-" ] && kill -TERM $$pid 2>/dev/null || true; done; \
|
||||
netstat -tlnp 2>/dev/null | grep :$(S3_PORT) | awk '{print $$7}' | cut -d/ -f1 | head -5 | while read pid; do [ "$$pid" != "-" ] && kill -TERM $$pid 2>/dev/null || true; done; \
|
||||
netstat -tlnp 2>/dev/null | grep :$$(( $(MASTER_PORT) + 10000 )) | awk '{print $$7}' | cut -d/ -f1 | head -5 | while read pid; do [ "$$pid" != "-" ] && kill -TERM $$pid 2>/dev/null || true; done; \
|
||||
netstat -tlnp 2>/dev/null | grep :$$(( $(VOLUME_PORT) + 10000 )) | awk '{print $$7}' | cut -d/ -f1 | head -5 | while read pid; do [ "$$pid" != "-" ] && kill -TERM $$pid 2>/dev/null || true; done; \
|
||||
netstat -tlnp 2>/dev/null | grep :$$(( $(FILER_PORT) + 10000 )) | awk '{print $$7}' | cut -d/ -f1 | head -5 | while read pid; do [ "$$pid" != "-" ] && kill -TERM $$pid 2>/dev/null || true; done; \
|
||||
fi
|
||||
@sleep 2
|
||||
@echo "$(GREEN)SeaweedFS server safely stopped$(NC)"
|
||||
@@ -263,14 +351,18 @@ test-implicit-dir-with-server: build-weed setup-python
|
||||
|
||||
# Debug targets
|
||||
debug-logs:
|
||||
@echo "$(YELLOW)=== Mini Log ===$(NC)"
|
||||
@tail -n 50 /tmp/seaweedfs-parquet-mini.log || echo "No mini log found"
|
||||
@echo "$(YELLOW)=== Master Log ===$(NC)"
|
||||
@tail -n 50 /tmp/seaweedfs-parquet-master.log || echo "No master log found"
|
||||
@echo "$(YELLOW)=== Volume Log ===$(NC)"
|
||||
@tail -n 50 /tmp/seaweedfs-parquet-volume.log || echo "No volume log found"
|
||||
@echo "$(YELLOW)=== Filer Log ===$(NC)"
|
||||
@tail -n 50 /tmp/seaweedfs-parquet-filer.log || echo "No filer log found"
|
||||
|
||||
debug-status:
|
||||
@echo "$(YELLOW)=== Process Status ===$(NC)"
|
||||
@ps aux | grep -E "(weed|seaweedfs)" | grep -v grep || echo "No SeaweedFS processes found"
|
||||
@echo "$(YELLOW)=== Port Status ===$(NC)"
|
||||
@netstat -an | grep -E "($(S3_PORT))" || echo "No ports in use"
|
||||
@netstat -an | grep -E "($(MASTER_PORT)|$(VOLUME_PORT)|$(FILER_PORT)|$(S3_PORT))" || echo "No ports in use"
|
||||
|
||||
# Manual test targets for development
|
||||
manual-start: start-seaweedfs
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# Test Coverage Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
This document provides comprehensive test coverage documentation for the SeaweedFS S3 Parquet integration tests.
|
||||
|
||||
## Test Categories
|
||||
|
||||
### Unit Tests (Go)
|
||||
- 17 test cases covering S3 API handlers
|
||||
- Tests for implicit directory handling
|
||||
- HEAD request behavior validation
|
||||
- Located in: `weed/s3api/s3api_implicit_directory_test.go`
|
||||
|
||||
### Integration Tests (Python)
|
||||
- 6 test cases for implicit directory fix
|
||||
- Tests HEAD request behavior on directory markers
|
||||
- s3fs directory detection validation
|
||||
- PyArrow dataset read compatibility
|
||||
- Located in: `test_implicit_directory_fix.py`
|
||||
|
||||
### End-to-End Tests (Python)
|
||||
- 20 test cases combining write and read methods
|
||||
- Small file tests (5 rows): 10 test combinations
|
||||
- Large file tests (200,000 rows): 10 test combinations
|
||||
- Tests multiple write methods: `pads.write_dataset`, `pq.write_table+s3fs`
|
||||
- Tests multiple read methods: `pads.dataset`, `pq.ParquetDataset`, `pq.read_table`, `s3fs+direct`, `s3fs+buffered`
|
||||
- Located in: `s3_parquet_test.py`
|
||||
|
||||
## Coverage Summary
|
||||
|
||||
| Test Type | Count | Status |
|
||||
|-----------|-------|--------|
|
||||
| Unit Tests (Go) | 17 | ✅ Pass |
|
||||
| Integration Tests (Python) | 6 | ✅ Pass |
|
||||
| End-to-End Tests (Python) | 20 | ✅ Pass |
|
||||
| **Total** | **43** | **✅ All Pass** |
|
||||
|
||||
## TODO
|
||||
|
||||
- [ ] Add detailed test execution time metrics
|
||||
- [ ] Document test data generation strategies
|
||||
- [ ] Add code coverage percentages for Go tests
|
||||
- [ ] Document edge cases and corner cases tested
|
||||
- [ ] Add performance benchmarking results
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Debug script to understand what pads.write_dataset creates."""
|
||||
|
||||
import sys
|
||||
import pyarrow as pa
|
||||
import pyarrow.dataset as pads
|
||||
import s3fs
|
||||
|
||||
# Create a simple test table
|
||||
table = pa.table({'id': [1, 2, 3], 'value': [1.0, 2.0, 3.0]})
|
||||
|
||||
# Initialize S3 filesystem
|
||||
fs = s3fs.S3FileSystem(
|
||||
client_kwargs={'endpoint_url': 'http://localhost:8333'},
|
||||
key='some_access_key1',
|
||||
secret='some_secret_key1',
|
||||
use_listings_cache=False,
|
||||
)
|
||||
|
||||
# Create bucket
|
||||
if not fs.exists('test-bucket'):
|
||||
fs.mkdir('test-bucket')
|
||||
|
||||
# Write with pads.write_dataset
|
||||
test_path = 's3://test-bucket/test-write-simple/'
|
||||
print(f"Writing to: {test_path}")
|
||||
print(f"Table schema: {table.schema}")
|
||||
print(f"Table rows: {table.num_rows}")
|
||||
|
||||
try:
|
||||
pads.write_dataset(table, test_path, format='parquet', filesystem=fs)
|
||||
print("\n✓ Write succeeded")
|
||||
|
||||
# List all files recursively
|
||||
print(f"\nListing all files recursively under {test_path}:")
|
||||
import os
|
||||
base_path = 'test-bucket/test-write-simple'
|
||||
def list_recursive(path, indent=0):
|
||||
try:
|
||||
items = fs.ls(path, detail=False)
|
||||
for item in items:
|
||||
is_dir = fs.isdir(item)
|
||||
item_name = item.split('/')[-1] if '/' in item else item
|
||||
if is_dir:
|
||||
print(f"{' ' * indent}📁 {item_name}/")
|
||||
list_recursive(item, indent + 1)
|
||||
else:
|
||||
# Get file size
|
||||
try:
|
||||
info = fs.info(item)
|
||||
size = info.get('size', 0)
|
||||
print(f"{' ' * indent}📄 {item_name} ({size} bytes)")
|
||||
except:
|
||||
print(f"{' ' * indent}📄 {item_name}")
|
||||
except Exception as e:
|
||||
print(f"{' ' * indent}Error listing {path}: {e}")
|
||||
|
||||
list_recursive(base_path)
|
||||
|
||||
# Try to read back with different methods
|
||||
print(f"\n\nTrying to read back using different methods:")
|
||||
|
||||
# Method 1: pads.dataset with the same path
|
||||
print(f"\n1. pads.dataset('{test_path}'):")
|
||||
try:
|
||||
dataset = pads.dataset(test_path, format='parquet', filesystem=fs)
|
||||
result = dataset.to_table()
|
||||
print(f" ✓ Success: {result.num_rows} rows")
|
||||
except Exception as e:
|
||||
print(f" ✗ Failed: {e}")
|
||||
|
||||
# Method 2: pads.dataset with the dir containing parquet files
|
||||
print(f"\n2. pads.dataset without trailing slash:")
|
||||
test_path_no_slash = 's3://test-bucket/test-write-simple'
|
||||
try:
|
||||
dataset = pads.dataset(test_path_no_slash, format='parquet', filesystem=fs)
|
||||
result = dataset.to_table()
|
||||
print(f" ✓ Success: {result.num_rows} rows")
|
||||
except Exception as e:
|
||||
print(f" ✗ Failed: {e}")
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
print(f"✗ Error: {e}")
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
@@ -60,7 +60,6 @@ def setup_s3():
|
||||
endpoint_url=S3_ENDPOINT_URL,
|
||||
aws_access_key_id=S3_ACCESS_KEY,
|
||||
aws_secret_access_key=S3_SECRET_KEY,
|
||||
region_name='us-east-1',
|
||||
use_ssl=False
|
||||
)
|
||||
|
||||
@@ -183,7 +182,7 @@ def test_explicit_directory_marker(fs, s3_client):
|
||||
logger.info("="*80)
|
||||
|
||||
# Create an explicit directory marker
|
||||
logger.info(f"Creating explicit directory: {BUCKET_NAME}/explicit_dir/")
|
||||
logger.info(f"\nCreating explicit directory: {BUCKET_NAME}/explicit_dir/")
|
||||
try:
|
||||
s3_client.put_object(
|
||||
Bucket=BUCKET_NAME,
|
||||
|
||||
@@ -10,17 +10,19 @@ all: test-with-server
|
||||
# Configuration
|
||||
WEED_BINARY := ../../../weed/weed_binary
|
||||
|
||||
ACCESS_KEY ?= some_access_key1
|
||||
SECRET_KEY ?= some_secret_key1
|
||||
|
||||
# Primary SeaweedFS (the one being tested - has remote caching)
|
||||
PRIMARY_S3_PORT := 8333
|
||||
PRIMARY_FILER_PORT := 8888
|
||||
PRIMARY_MASTER_PORT := 9333
|
||||
PRIMARY_VOLUME_PORT := 8080
|
||||
PRIMARY_METRICS_PORT := 9324
|
||||
PRIMARY_DIR := ./test-primary-data
|
||||
|
||||
# Secondary SeaweedFS (acts as "remote" S3 storage)
|
||||
REMOTE_S3_PORT := 8334
|
||||
REMOTE_FILER_PORT := 8889
|
||||
REMOTE_MASTER_PORT := 9334
|
||||
REMOTE_VOLUME_PORT := 8081
|
||||
REMOTE_METRICS_PORT := 9325
|
||||
REMOTE_DIR := ./test-remote-data
|
||||
|
||||
@@ -71,11 +73,18 @@ start-remote: check-deps
|
||||
@echo "Starting remote SeaweedFS (secondary instance)..."
|
||||
@rm -f remote-server.pid
|
||||
@mkdir -p $(REMOTE_DIR)
|
||||
@AWS_ACCESS_KEY_ID=$(ACCESS_KEY) AWS_SECRET_ACCESS_KEY=$(SECRET_KEY) $(WEED_BINARY) mini \
|
||||
@$(WEED_BINARY) server \
|
||||
-s3 \
|
||||
-s3.port=$(REMOTE_S3_PORT) \
|
||||
-s3.allowDeleteBucketNotEmpty=true \
|
||||
-filer \
|
||||
-filer.port=$(REMOTE_FILER_PORT) \
|
||||
-master.port=$(REMOTE_MASTER_PORT) \
|
||||
-volume.port=$(REMOTE_VOLUME_PORT) \
|
||||
-master.volumeSizeLimitMB=50 \
|
||||
-volume.max=100 \
|
||||
-dir=$(REMOTE_DIR) \
|
||||
-ip.bind=0.0.0.0 \
|
||||
-volume.preStopSeconds=1 \
|
||||
-metricsPort=$(REMOTE_METRICS_PORT) \
|
||||
> remote-weed.log 2>&1 & echo $$! > remote-server.pid
|
||||
@echo "Waiting for remote SeaweedFS to start..."
|
||||
@@ -84,7 +93,7 @@ start-remote: check-deps
|
||||
echo "Remote SeaweedFS started on port $(REMOTE_S3_PORT)"; \
|
||||
exit 0; \
|
||||
fi; \
|
||||
sleep 3; \
|
||||
sleep 1; \
|
||||
done; \
|
||||
echo "ERROR: Remote SeaweedFS failed to start"; \
|
||||
cat remote-weed.log; \
|
||||
@@ -105,11 +114,18 @@ start-primary: check-deps
|
||||
@echo "Starting primary SeaweedFS..."
|
||||
@rm -f primary-server.pid
|
||||
@mkdir -p $(PRIMARY_DIR)
|
||||
@AWS_ACCESS_KEY_ID=$(ACCESS_KEY) AWS_SECRET_ACCESS_KEY=$(SECRET_KEY) $(WEED_BINARY) mini \
|
||||
@$(WEED_BINARY) server \
|
||||
-s3 \
|
||||
-s3.port=$(PRIMARY_S3_PORT) \
|
||||
-s3.allowDeleteBucketNotEmpty=true \
|
||||
-filer \
|
||||
-filer.port=$(PRIMARY_FILER_PORT) \
|
||||
-master.port=$(PRIMARY_MASTER_PORT) \
|
||||
-volume.port=$(PRIMARY_VOLUME_PORT) \
|
||||
-master.volumeSizeLimitMB=50 \
|
||||
-volume.max=100 \
|
||||
-dir=$(PRIMARY_DIR) \
|
||||
-ip.bind=0.0.0.0 \
|
||||
-volume.preStopSeconds=1 \
|
||||
-metricsPort=$(PRIMARY_METRICS_PORT) \
|
||||
> primary-weed.log 2>&1 & echo $$! > primary-server.pid
|
||||
@echo "Waiting for primary SeaweedFS to start..."
|
||||
@@ -118,7 +134,7 @@ start-primary: check-deps
|
||||
echo "Primary SeaweedFS started on port $(PRIMARY_S3_PORT)"; \
|
||||
exit 0; \
|
||||
fi; \
|
||||
sleep 3; \
|
||||
sleep 1; \
|
||||
done; \
|
||||
echo "ERROR: Primary SeaweedFS failed to start"; \
|
||||
cat primary-weed.log; \
|
||||
@@ -140,7 +156,7 @@ setup-remote:
|
||||
@curl -s -X PUT "http://localhost:$(REMOTE_S3_PORT)/$(REMOTE_BUCKET)" || echo "Bucket may already exist"
|
||||
@sleep 1
|
||||
@echo "Configuring remote storage on primary..."
|
||||
@printf 'remote.configure -name=seaweedremote -type=s3 -s3.access_key=$(ACCESS_KEY) -s3.secret_key=$(SECRET_KEY) -s3.endpoint=http://localhost:$(REMOTE_S3_PORT) -s3.region=us-east-1\nexit\n' | $(WEED_BINARY) shell -master=localhost:$(PRIMARY_MASTER_PORT) 2>&1 || echo "remote.configure done"
|
||||
@printf 'remote.configure -name=seaweedremote -type=s3 -s3.access_key=any -s3.secret_key=any -s3.endpoint=http://localhost:$(REMOTE_S3_PORT) -s3.region=us-east-1\nexit\n' | $(WEED_BINARY) shell -master=localhost:$(PRIMARY_MASTER_PORT) 2>&1 || echo "remote.configure done"
|
||||
@sleep 2
|
||||
@echo "Mounting remote bucket on primary..."
|
||||
@printf 'remote.mount -dir=/buckets/remotemounted -remote=seaweedremote/$(REMOTE_BUCKET) -nonempty\nexit\n' | $(WEED_BINARY) shell -master=localhost:$(PRIMARY_MASTER_PORT) 2>&1 || echo "remote.mount done"
|
||||
|
||||
@@ -34,8 +34,8 @@ const (
|
||||
remoteEndpoint = "http://localhost:8334"
|
||||
|
||||
// Credentials (anonymous access for testing)
|
||||
accessKey = "some_access_key1"
|
||||
secretKey = "some_secret_key1"
|
||||
accessKey = "any"
|
||||
secretKey = "any"
|
||||
|
||||
// Bucket name - mounted on primary as remote storage
|
||||
testBucket = "remotemounted"
|
||||
@@ -121,6 +121,17 @@ func getFromPrimary(t *testing.T, key string) []byte {
|
||||
return data
|
||||
}
|
||||
|
||||
// syncToRemote syncs local data to remote storage
|
||||
func syncToRemote(t *testing.T) {
|
||||
t.Log("Syncing to remote storage...")
|
||||
output, err := runWeedShell(t, "remote.cache.uncache -dir=/buckets/"+testBucket+" -include=*")
|
||||
if err != nil {
|
||||
t.Logf("syncToRemote warning: %v", err)
|
||||
}
|
||||
t.Log(output)
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
// uncacheLocal purges the local cache, forcing data to be fetched from remote
|
||||
func uncacheLocal(t *testing.T, pattern string) {
|
||||
t.Logf("Purging local cache for pattern: %s", pattern)
|
||||
|
||||
+19
-12
@@ -6,14 +6,11 @@
|
||||
# Configuration
|
||||
WEED_BINARY := ../../../weed/weed_binary
|
||||
S3_PORT := 8333
|
||||
ACCESS_KEY ?= some_access_key1
|
||||
SECRET_KEY ?= some_secret_key1
|
||||
MASTER_PORT := 9333
|
||||
VOLUME_PORT := 8080
|
||||
FILER_PORT := 8888
|
||||
TEST_TIMEOUT := 15m
|
||||
TEST_PATTERN := TestRetention
|
||||
SERVER_DIR := ./test-volume-data/server-data
|
||||
|
||||
# Default target
|
||||
help:
|
||||
@@ -83,16 +80,23 @@ start-server: check-deps
|
||||
@ls -la ../../../docker/compose/s3.json || echo "⚠️ Config file not found, continuing without it"
|
||||
@echo "🔍 DEBUG: Creating volume directory..."
|
||||
@mkdir -p ./test-volume-data
|
||||
@echo "🔍 DEBUG: Creating server data directory..."
|
||||
@mkdir -p $(SERVER_DIR)
|
||||
@echo "🔍 DEBUG: Launching SeaweedFS S3 server in background..."
|
||||
@echo "🔍 DEBUG: Command: $(WEED_BINARY) mini -dir=$(SERVER_DIR) -s3.port=$(S3_PORT)"
|
||||
@AWS_ACCESS_KEY_ID=$(ACCESS_KEY) AWS_SECRET_ACCESS_KEY=$(SECRET_KEY) $(WEED_BINARY) mini \
|
||||
-dir=$(SERVER_DIR) \
|
||||
@echo "🔍 DEBUG: Launching SeaweedFS server in background..."
|
||||
@echo "🔍 DEBUG: Command: $(WEED_BINARY) server -debug -s3 -s3.port=$(S3_PORT) -s3.allowDeleteBucketNotEmpty=true -s3.config=../../../docker/compose/s3.json -filer -filer.maxMB=64 -master.volumeSizeLimitMB=50 -volume.max=100 -dir=./test-volume-data -volume.preStopSeconds=1 -metricsPort=9324"
|
||||
@$(WEED_BINARY) server \
|
||||
-debug \
|
||||
-s3 \
|
||||
-s3.port=$(S3_PORT) \
|
||||
> weed-test.log 2>&1 & \
|
||||
echo $$! > weed-server.pid
|
||||
@echo "🔍 DEBUG: Server PID: $$(cat weed-test.pid 2>/dev/null || echo 'PID file not found')"
|
||||
-s3.allowDeleteBucketNotEmpty=true \
|
||||
-s3.config=../../../docker/compose/s3.json \
|
||||
-filer \
|
||||
-filer.maxMB=64 \
|
||||
-master.volumeSizeLimitMB=50 \
|
||||
-volume.max=100 \
|
||||
-dir=./test-volume-data \
|
||||
-volume.preStopSeconds=1 \
|
||||
-metricsPort=9324 \
|
||||
> weed-test.log 2>&1 & echo $$! > weed-server.pid
|
||||
@echo "🔍 DEBUG: Server PID: $$(cat weed-server.pid 2>/dev/null || echo 'PID file not found')"
|
||||
@echo "🔍 DEBUG: Checking if PID is still running..."
|
||||
@sleep 2
|
||||
@if [ -f weed-server.pid ]; then \
|
||||
@@ -101,6 +105,7 @@ start-server: check-deps
|
||||
else \
|
||||
echo "⚠️ PID file not found"; \
|
||||
fi
|
||||
@echo "🔍 DEBUG: Waiting for server to start (up to 90 seconds)..."
|
||||
@for i in $$(seq 1 90); do \
|
||||
echo "🔍 DEBUG: Attempt $$i/90 - checking port $(S3_PORT)"; \
|
||||
if curl -s http://localhost:$(S3_PORT) >/dev/null 2>&1; then \
|
||||
@@ -118,6 +123,8 @@ start-server: check-deps
|
||||
if [ $$i -eq 15 ]; then \
|
||||
echo "🔍 DEBUG: After 15 seconds, checking port bindings..."; \
|
||||
netstat -tlnp 2>/dev/null | grep $(S3_PORT) || echo "Port $(S3_PORT) not bound"; \
|
||||
netstat -tlnp 2>/dev/null | grep 9333 || echo "Port 9333 not bound"; \
|
||||
netstat -tlnp 2>/dev/null | grep 8080 || echo "Port 8080 not bound"; \
|
||||
fi; \
|
||||
if [ $$i -eq 30 ]; then \
|
||||
echo "⚠️ Server taking longer than expected (30s), checking logs..."; \
|
||||
|
||||
@@ -2,13 +2,12 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/aws/retry"
|
||||
"github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go-v2/credentials"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
+86
-62
@@ -93,35 +93,54 @@ start-seaweedfs: check-binary
|
||||
@sleep 2
|
||||
|
||||
# Create necessary directories
|
||||
@mkdir -p /tmp/seaweedfs-test-sse
|
||||
@mkdir -p /tmp/seaweedfs-test-sse-master
|
||||
@mkdir -p /tmp/seaweedfs-test-sse-volume
|
||||
@mkdir -p /tmp/seaweedfs-test-sse-filer
|
||||
|
||||
# Start master server with volume size limit and explicit gRPC port
|
||||
@nohup $(SEAWEEDFS_BINARY) master -port=$(MASTER_PORT) -port.grpc=$$(( $(MASTER_PORT) + 10000 )) -mdir=/tmp/seaweedfs-test-sse-master -volumeSizeLimitMB=$(VOLUME_MAX_SIZE_MB) -ip=127.0.0.1 -peers=none > /tmp/seaweedfs-sse-master.log 2>&1 &
|
||||
@sleep 3
|
||||
|
||||
# Start volume server with master HTTP port and increased capacity
|
||||
@nohup $(SEAWEEDFS_BINARY) volume -port=$(VOLUME_PORT) -master=127.0.0.1:$(MASTER_PORT) -dir=/tmp/seaweedfs-test-sse-volume -max=$(VOLUME_MAX_COUNT) -ip=127.0.0.1 > /tmp/seaweedfs-sse-volume.log 2>&1 &
|
||||
@sleep 5
|
||||
|
||||
# Start filer server (using standard SeaweedFS gRPC port convention: HTTP port + 10000)
|
||||
@nohup $(SEAWEEDFS_BINARY) filer -port=$(FILER_PORT) -port.grpc=$$(( $(FILER_PORT) + 10000 )) -master=127.0.0.1:$(MASTER_PORT) -dataCenter=defaultDataCenter -ip=127.0.0.1 > /tmp/seaweedfs-sse-filer.log 2>&1 &
|
||||
@sleep 3
|
||||
|
||||
# Create S3 configuration with SSE-KMS support
|
||||
@printf '{"identities":[{"name":"%s","credentials":[{"accessKey":"%s","secretKey":"%s"}],"actions":["Admin","Read","Write"]}],"kms":{"type":"%s","configs":{"keyId":"%s","encryptionContext":{},"bucketKey":false}}}' "$(ACCESS_KEY)" "$(ACCESS_KEY)" "$(SECRET_KEY)" "$(KMS_TYPE)" "$(KMS_KEY_ID)" > /tmp/seaweedfs-sse-s3.json
|
||||
|
||||
# Start weed mini
|
||||
@AWS_ACCESS_KEY_ID=$(ACCESS_KEY) AWS_SECRET_ACCESS_KEY=$(SECRET_KEY) $(SEAWEEDFS_BINARY) mini \
|
||||
-dir=/tmp/seaweedfs-test-sse \
|
||||
-s3.port=$(S3_PORT) \
|
||||
-s3.config=/tmp/seaweedfs-sse-s3.json \
|
||||
> /tmp/seaweedfs-sse-mini.log 2>&1 & echo $$! > /tmp/weed-mini.pid
|
||||
|
||||
@echo "Checking S3 service is ready..."
|
||||
# Start S3 server with KMS configuration
|
||||
@nohup $(SEAWEEDFS_BINARY) s3 -port=$(S3_PORT) -filer=127.0.0.1:$(FILER_PORT) -config=/tmp/seaweedfs-sse-s3.json -ip.bind=127.0.0.1 > /tmp/seaweedfs-sse-s3.log 2>&1 &
|
||||
@sleep 5
|
||||
|
||||
# Wait for S3 service to be ready
|
||||
@echo "$(YELLOW)Waiting for S3 service to be ready...$(NC)"
|
||||
@for i in $$(seq 1 30); do \
|
||||
if curl -s http://127.0.0.1:$(S3_PORT) > /dev/null 2>&1; then \
|
||||
echo "✅ S3 service is ready"; \
|
||||
if curl -s -f http://127.0.0.1:$(S3_PORT) > /dev/null 2>&1; then \
|
||||
echo "$(GREEN)S3 service is ready$(NC)"; \
|
||||
break; \
|
||||
fi; \
|
||||
echo "Waiting for S3 service... ($$i/30)"; \
|
||||
sleep 1; \
|
||||
done
|
||||
|
||||
# Additional wait for filer gRPC to be ready
|
||||
@echo "$(YELLOW)Waiting for filer gRPC to be ready...$(NC)"
|
||||
@sleep 2
|
||||
@echo "$(GREEN)SeaweedFS server started successfully for SSE testing$(NC)"
|
||||
@echo "Master: http://localhost:$(MASTER_PORT)"
|
||||
@echo "Volume: http://localhost:$(VOLUME_PORT)"
|
||||
@echo "Filer: http://localhost:$(FILER_PORT)"
|
||||
@echo "S3: http://localhost:$(S3_PORT)"
|
||||
@echo "Volume Max Size: $(VOLUME_MAX_SIZE_MB)MB"
|
||||
@echo "SSE-KMS Support: Enabled"
|
||||
|
||||
stop-seaweedfs:
|
||||
@echo "$(YELLOW)Stopping SeaweedFS server...$(NC)"
|
||||
@# Use port-based cleanup for consistency and safety
|
||||
@if [ -f /tmp/weed-mini.pid ]; then \
|
||||
echo "Stopping weed mini..."; \
|
||||
kill $$(cat /tmp/weed-mini.pid) || true; \
|
||||
rm -f /tmp/weed-mini.pid; \
|
||||
fi
|
||||
@lsof -ti :$(MASTER_PORT) | xargs -r kill -TERM || true
|
||||
@lsof -ti :$(VOLUME_PORT) | xargs -r kill -TERM || true
|
||||
@lsof -ti :$(FILER_PORT) | xargs -r kill -TERM || true
|
||||
@@ -326,33 +345,71 @@ start-seaweedfs-ci: check-binary
|
||||
@echo "$(YELLOW)Starting SeaweedFS server for CI testing...$(NC)"
|
||||
|
||||
# Create necessary directories
|
||||
@mkdir -p /tmp/seaweedfs-test-sse
|
||||
@mkdir -p /tmp/seaweedfs-test-sse-master
|
||||
@mkdir -p /tmp/seaweedfs-test-sse-volume
|
||||
@mkdir -p /tmp/seaweedfs-test-sse-filer
|
||||
|
||||
# Clean up any old server logs
|
||||
@rm -f /tmp/seaweedfs-sse-*.log || true
|
||||
|
||||
# Start master server with volume size limit and explicit gRPC port
|
||||
@echo "Starting master server..."
|
||||
@nohup $(SEAWEEDFS_BINARY) master -port=$(MASTER_PORT) -port.grpc=$$(( $(MASTER_PORT) + 10000 )) -mdir=/tmp/seaweedfs-test-sse-master -volumeSizeLimitMB=$(VOLUME_MAX_SIZE_MB) -ip=127.0.0.1 -peers=none > /tmp/seaweedfs-sse-master.log 2>&1 &
|
||||
@sleep 3
|
||||
|
||||
# Start volume server with master HTTP port and increased capacity
|
||||
@echo "Starting volume server..."
|
||||
@nohup $(SEAWEEDFS_BINARY) volume -port=$(VOLUME_PORT) -master=127.0.0.1:$(MASTER_PORT) -dir=/tmp/seaweedfs-test-sse-volume -max=$(VOLUME_MAX_COUNT) -ip=127.0.0.1 > /tmp/seaweedfs-sse-volume.log 2>&1 &
|
||||
@sleep 5
|
||||
|
||||
# Create S3 JSON configuration with KMS (Local provider) and basic identity for embedded S3
|
||||
@sed -e 's/ACCESS_KEY_PLACEHOLDER/$(ACCESS_KEY)/g' \
|
||||
-e 's/SECRET_KEY_PLACEHOLDER/$(SECRET_KEY)/g' \
|
||||
s3-config-template.json > /tmp/seaweedfs-s3.json
|
||||
|
||||
# Start weed mini with embedded S3 using the JSON config (with verbose logging)
|
||||
@echo "Starting weed mini with embedded S3..."
|
||||
@AWS_ACCESS_KEY_ID=$(ACCESS_KEY) AWS_SECRET_ACCESS_KEY=$(SECRET_KEY) GLOG_v=4 $(SEAWEEDFS_BINARY) mini \
|
||||
-dir=/tmp/seaweedfs-test-sse \
|
||||
-s3.port=$(S3_PORT) \
|
||||
-s3.config=/tmp/seaweedfs-s3.json \
|
||||
-ip=127.0.0.1 \
|
||||
> /tmp/seaweedfs-sse-mini.log 2>&1 & echo $$! > /tmp/weed-mini.pid
|
||||
# Start filer server with embedded S3 using the JSON config (with verbose logging)
|
||||
@echo "Starting filer server with embedded S3..."
|
||||
@AWS_ACCESS_KEY_ID=$(ACCESS_KEY) AWS_SECRET_ACCESS_KEY=$(SECRET_KEY) GLOG_v=4 nohup $(SEAWEEDFS_BINARY) filer -port=$(FILER_PORT) -port.grpc=$$(( $(FILER_PORT) + 10000 )) -master=127.0.0.1:$(MASTER_PORT) -dataCenter=defaultDataCenter -ip=127.0.0.1 -s3 -s3.port=$(S3_PORT) -s3.config=/tmp/seaweedfs-s3.json > /tmp/seaweedfs-sse-filer.log 2>&1 &
|
||||
@sleep 5
|
||||
|
||||
@echo "Checking S3 service is ready..."
|
||||
@for i in $$(seq 1 30); do \
|
||||
if curl -s http://127.0.0.1:$(S3_PORT) > /dev/null 2>&1; then \
|
||||
echo "✅ S3 service is ready"; \
|
||||
# Wait for S3 service to be ready - use port-based checking for reliability
|
||||
@echo "$(YELLOW)Waiting for S3 service to be ready...$(NC)"
|
||||
@for i in $$(seq 1 20); do \
|
||||
if netstat -an 2>/dev/null | grep -q ":$(S3_PORT).*LISTEN" || \
|
||||
ss -an 2>/dev/null | grep -q ":$(S3_PORT).*LISTEN" || \
|
||||
lsof -i :$(S3_PORT) >/dev/null 2>&1; then \
|
||||
echo "$(GREEN)S3 service is listening on port $(S3_PORT)$(NC)"; \
|
||||
sleep 1; \
|
||||
break; \
|
||||
fi; \
|
||||
if [ $$i -eq 20 ]; then \
|
||||
echo "$(RED)S3 service failed to start within 20 seconds$(NC)"; \
|
||||
echo "=== Detailed Logs ==="; \
|
||||
echo "Master log:"; tail -30 /tmp/seaweedfs-sse-master.log || true; \
|
||||
echo "Volume log:"; tail -30 /tmp/seaweedfs-sse-volume.log || true; \
|
||||
echo "Filer log:"; tail -30 /tmp/seaweedfs-sse-filer.log || true; \
|
||||
echo "=== Port Status ==="; \
|
||||
netstat -an 2>/dev/null | grep ":$(S3_PORT)" || \
|
||||
ss -an 2>/dev/null | grep ":$(S3_PORT)" || \
|
||||
echo "No port listening on $(S3_PORT)"; \
|
||||
echo "=== Process Status ==="; \
|
||||
ps aux | grep -E "weed.*(filer|s3).*$(S3_PORT)" | grep -v grep || echo "No S3 process found"; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
echo "Waiting for S3 service... ($$i/20)"; \
|
||||
sleep 1; \
|
||||
done
|
||||
|
||||
# Additional wait for filer gRPC to be ready
|
||||
@echo "$(YELLOW)Waiting for filer gRPC to be ready...$(NC)"
|
||||
@sleep 2
|
||||
@echo "$(GREEN)SeaweedFS server started successfully for SSE testing$(NC)"
|
||||
@echo "Master: http://localhost:$(MASTER_PORT)"
|
||||
@echo "Volume: http://localhost:$(VOLUME_PORT)"
|
||||
@echo "Filer: http://localhost:$(FILER_PORT)"
|
||||
@echo "S3: http://localhost:$(S3_PORT)"
|
||||
@echo "Volume Max Size: $(VOLUME_MAX_SIZE_MB)MB"
|
||||
@echo "SSE-KMS Support: Enabled"
|
||||
|
||||
# GitHub Actions compatible quick test subset
|
||||
test-quick-with-server: build-weed
|
||||
@@ -470,36 +527,3 @@ dev-kms: setup-openbao
|
||||
@echo "OpenBao: $(OPENBAO_ADDR)"
|
||||
@echo "Token: $(OPENBAO_TOKEN)"
|
||||
@echo "Use 'make test-ssekms-integration' to run tests"
|
||||
|
||||
# Volume encryption integration tests
|
||||
test-volume-encryption: build-weed
|
||||
@echo "🚀 Starting S3 volume encryption integration tests..."
|
||||
@echo "Starting SeaweedFS cluster with volume encryption enabled..."
|
||||
@# Start server with -s3.encryptVolumeData flag
|
||||
@mkdir -p /tmp/seaweedfs-test-sse
|
||||
@rm -f /tmp/seaweedfs-sse-*.log || true
|
||||
@sed -e 's/ACCESS_KEY_PLACEHOLDER/$(ACCESS_KEY)/g' \
|
||||
-e 's/SECRET_KEY_PLACEHOLDER/$(SECRET_KEY)/g' \
|
||||
s3-config-template.json > /tmp/seaweedfs-s3.json
|
||||
@echo "Starting weed mini with S3 volume encryption..."
|
||||
@AWS_ACCESS_KEY_ID=$(ACCESS_KEY) AWS_SECRET_ACCESS_KEY=$(SECRET_KEY) GLOG_v=4 $(SEAWEEDFS_BINARY) mini \
|
||||
-dir=/tmp/seaweedfs-test-sse \
|
||||
-s3.port=$(S3_PORT) \
|
||||
-s3.config=/tmp/seaweedfs-s3.json \
|
||||
-s3.encryptVolumeData \
|
||||
-ip=127.0.0.1 \
|
||||
> /tmp/seaweedfs-sse-mini.log 2>&1 & echo $$! > /tmp/weed-mini.pid
|
||||
@echo "Checking S3 service is ready..."
|
||||
@for i in $$(seq 1 30); do \
|
||||
if curl -s http://127.0.0.1:$(S3_PORT) > /dev/null 2>&1; then \
|
||||
echo "✅ S3 service is ready"; \
|
||||
break; \
|
||||
fi; \
|
||||
sleep 1; \
|
||||
done
|
||||
@echo "Running volume encryption integration tests..."
|
||||
@trap '$(MAKE) -C $(TEST_DIR) stop-seaweedfs-safe || true' EXIT; \
|
||||
cd $(SEAWEEDFS_ROOT) && go test -v -tags=integration -timeout=10m -run "TestS3VolumeEncryption" ./test/s3/sse || exit 1; \
|
||||
echo "✅ Volume encryption tests completed successfully"; \
|
||||
$(MAKE) -C $(TEST_DIR) stop-seaweedfs-safe || true
|
||||
|
||||
|
||||
@@ -502,3 +502,4 @@ func TestGitHub7562LargeFile(t *testing.T) {
|
||||
|
||||
t.Log("Large file test passed!")
|
||||
}
|
||||
|
||||
|
||||
@@ -1,448 +0,0 @@
|
||||
//go:build integration
|
||||
// +build integration
|
||||
|
||||
package sse
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
)
|
||||
|
||||
// TestS3VolumeEncryptionRoundtrip tests that data uploaded with encryptVolumeData
|
||||
// enabled can be read back correctly. This requires the S3 server to be started with
|
||||
// -encryptVolumeData=true for the test to verify encryption is working.
|
||||
//
|
||||
// To run this test:
|
||||
// 1. Start SeaweedFS: weed server -s3 -s3.encryptVolumeData=true
|
||||
// 2. Run: go test -v -run TestS3VolumeEncryptionRoundtrip
|
||||
func TestS3VolumeEncryptionRoundtrip(t *testing.T) {
|
||||
svc := getS3Client(t)
|
||||
bucket := fmt.Sprintf("volume-encryption-test-%d", time.Now().Unix())
|
||||
|
||||
// Create bucket
|
||||
_, err := svc.CreateBucket(&s3.CreateBucketInput{
|
||||
Bucket: aws.String(bucket),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create bucket: %v", err)
|
||||
}
|
||||
defer cleanupBucket(t, svc, bucket)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
key string
|
||||
content string
|
||||
rangeReq string // Optional range request
|
||||
}{
|
||||
{
|
||||
name: "small file",
|
||||
key: "small.txt",
|
||||
content: "Hello, encrypted world!",
|
||||
},
|
||||
{
|
||||
name: "medium file",
|
||||
key: "medium.txt",
|
||||
content: strings.Repeat("SeaweedFS volume encryption test content. ", 1000),
|
||||
},
|
||||
{
|
||||
name: "binary content",
|
||||
key: "binary.bin",
|
||||
content: string([]byte{0x00, 0x01, 0x02, 0xFF, 0xFE, 0xFD, 0x00, 0x80}),
|
||||
},
|
||||
{
|
||||
name: "range request",
|
||||
key: "range-test.txt",
|
||||
content: "0123456789ABCDEFGHIJ",
|
||||
rangeReq: "bytes=5-10",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Upload
|
||||
_, err := svc.PutObject(&s3.PutObjectInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Key: aws.String(tc.key),
|
||||
Body: strings.NewReader(tc.content),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("PutObject failed: %v", err)
|
||||
}
|
||||
|
||||
// Download
|
||||
getInput := &s3.GetObjectInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Key: aws.String(tc.key),
|
||||
}
|
||||
if tc.rangeReq != "" {
|
||||
getInput.Range = aws.String(tc.rangeReq)
|
||||
}
|
||||
|
||||
result, err := svc.GetObject(getInput)
|
||||
if err != nil {
|
||||
t.Fatalf("GetObject failed: %v", err)
|
||||
}
|
||||
defer result.Body.Close()
|
||||
|
||||
data, err := io.ReadAll(result.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read response body: %v", err)
|
||||
}
|
||||
|
||||
// Verify content
|
||||
expected := tc.content
|
||||
if tc.rangeReq != "" {
|
||||
// For "bytes=5-10", we expect characters at positions 5-10 (inclusive)
|
||||
expected = tc.content[5:11]
|
||||
}
|
||||
|
||||
if string(data) != expected {
|
||||
t.Errorf("Content mismatch:\n expected: %q\n got: %q", expected, string(data))
|
||||
} else {
|
||||
t.Logf("Successfully uploaded and downloaded %s (%d bytes)", tc.key, len(data))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestS3VolumeEncryptionMultiChunk tests large files that span multiple chunks
|
||||
// to ensure encryption works correctly across chunk boundaries.
|
||||
func TestS3VolumeEncryptionMultiChunk(t *testing.T) {
|
||||
svc := getS3Client(t)
|
||||
bucket := fmt.Sprintf("volume-encryption-multichunk-%d", time.Now().Unix())
|
||||
|
||||
// Create bucket
|
||||
_, err := svc.CreateBucket(&s3.CreateBucketInput{
|
||||
Bucket: aws.String(bucket),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create bucket: %v", err)
|
||||
}
|
||||
defer cleanupBucket(t, svc, bucket)
|
||||
|
||||
// Create a file larger than default chunk size (8MB)
|
||||
// Use 10MB to ensure multiple chunks
|
||||
largeContent := make([]byte, 10*1024*1024)
|
||||
for i := range largeContent {
|
||||
largeContent[i] = byte(i % 256)
|
||||
}
|
||||
|
||||
key := "large-file.bin"
|
||||
|
||||
// Upload
|
||||
_, err = svc.PutObject(&s3.PutObjectInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Key: aws.String(key),
|
||||
Body: bytes.NewReader(largeContent),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("PutObject failed for large file: %v", err)
|
||||
}
|
||||
|
||||
// Download full file
|
||||
result, err := svc.GetObject(&s3.GetObjectInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Key: aws.String(key),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("GetObject failed: %v", err)
|
||||
}
|
||||
defer result.Body.Close()
|
||||
|
||||
downloadedData, err := io.ReadAll(result.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read response body: %v", err)
|
||||
}
|
||||
|
||||
if len(downloadedData) != len(largeContent) {
|
||||
t.Errorf("Size mismatch: expected %d, got %d", len(largeContent), len(downloadedData))
|
||||
}
|
||||
|
||||
if !bytes.Equal(downloadedData, largeContent) {
|
||||
t.Errorf("Content mismatch in multi-chunk file")
|
||||
// Find first mismatch
|
||||
for i := 0; i < len(downloadedData) && i < len(largeContent); i++ {
|
||||
if downloadedData[i] != largeContent[i] {
|
||||
t.Errorf("First mismatch at byte %d: expected %02x, got %02x", i, largeContent[i], downloadedData[i])
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
t.Logf("Successfully uploaded and downloaded %d byte multi-chunk file", len(downloadedData))
|
||||
}
|
||||
|
||||
// Test range request spanning chunk boundary (around 8MB)
|
||||
rangeStart := int64(8*1024*1024 - 1000)
|
||||
rangeEnd := int64(8*1024*1024 + 1000)
|
||||
rangeResult, err := svc.GetObject(&s3.GetObjectInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Key: aws.String(key),
|
||||
Range: aws.String(fmt.Sprintf("bytes=%d-%d", rangeStart, rangeEnd)),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Range GetObject failed: %v", err)
|
||||
}
|
||||
defer rangeResult.Body.Close()
|
||||
|
||||
rangeData, err := io.ReadAll(rangeResult.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read range response: %v", err)
|
||||
}
|
||||
|
||||
expectedRange := largeContent[rangeStart : rangeEnd+1]
|
||||
if !bytes.Equal(rangeData, expectedRange) {
|
||||
t.Errorf("Range request content mismatch at chunk boundary")
|
||||
} else {
|
||||
t.Logf("Successfully retrieved range spanning chunk boundary (%d bytes)", len(rangeData))
|
||||
}
|
||||
}
|
||||
|
||||
// TestS3VolumeEncryptionMultiChunkRangeRead tests range reads that span multiple chunks:
|
||||
// - Part of chunk 1
|
||||
// - Whole chunk 2
|
||||
// - Part of chunk 3
|
||||
// This is critical for verifying that cipher decryption works correctly when
|
||||
// reading across chunk boundaries with the cipherKey from each chunk.
|
||||
func TestS3VolumeEncryptionMultiChunkRangeRead(t *testing.T) {
|
||||
svc := getS3Client(t)
|
||||
bucket := fmt.Sprintf("volume-encryption-multirange-%d", time.Now().Unix())
|
||||
|
||||
// Create bucket
|
||||
_, err := svc.CreateBucket(&s3.CreateBucketInput{
|
||||
Bucket: aws.String(bucket),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create bucket: %v", err)
|
||||
}
|
||||
defer cleanupBucket(t, svc, bucket)
|
||||
|
||||
// Default chunk size is 8MB. Create a file with 3+ chunks (25MB)
|
||||
// to ensure we have multiple complete chunks
|
||||
const chunkSize = 8 * 1024 * 1024 // 8MB
|
||||
const fileSize = 25 * 1024 * 1024 // 25MB = 3 chunks + partial 4th
|
||||
|
||||
largeContent := make([]byte, fileSize)
|
||||
for i := range largeContent {
|
||||
// Use recognizable pattern: each byte encodes its position
|
||||
largeContent[i] = byte(i % 256)
|
||||
}
|
||||
|
||||
key := "multi-chunk-range-test.bin"
|
||||
|
||||
// Upload
|
||||
_, err = svc.PutObject(&s3.PutObjectInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Key: aws.String(key),
|
||||
Body: bytes.NewReader(largeContent),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("PutObject failed: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("Uploaded %d byte file (%d chunks of %d bytes)", fileSize, (fileSize+chunkSize-1)/chunkSize, chunkSize)
|
||||
|
||||
// Test cases for range reads spanning multiple chunks
|
||||
testCases := []struct {
|
||||
name string
|
||||
rangeStart int64
|
||||
rangeEnd int64
|
||||
desc string
|
||||
}{
|
||||
{
|
||||
name: "part_chunk1_whole_chunk2_part_chunk3",
|
||||
rangeStart: chunkSize - 1000, // 1000 bytes before end of chunk 1
|
||||
rangeEnd: 2*chunkSize + 1000, // 1000 bytes into chunk 3
|
||||
desc: "Spans from end of chunk 1 through entire chunk 2 into beginning of chunk 3",
|
||||
},
|
||||
{
|
||||
name: "last_byte_chunk1_through_first_byte_chunk3",
|
||||
rangeStart: chunkSize - 1, // Last byte of chunk 1
|
||||
rangeEnd: 2 * chunkSize, // First byte of chunk 3
|
||||
desc: "Minimal span: last byte of chunk 1, entire chunk 2, first byte of chunk 3",
|
||||
},
|
||||
{
|
||||
name: "middle_chunk1_to_middle_chunk3",
|
||||
rangeStart: chunkSize / 2, // Middle of chunk 1
|
||||
rangeEnd: 2*chunkSize + chunkSize/2, // Middle of chunk 3
|
||||
desc: "From middle of chunk 1 to middle of chunk 3",
|
||||
},
|
||||
{
|
||||
name: "half_chunk1_whole_chunk2_half_chunk3",
|
||||
rangeStart: chunkSize / 2, // Half of chunk 1
|
||||
rangeEnd: 2*chunkSize + chunkSize/2 - 1, // Half of chunk 3
|
||||
desc: "Half of each boundary chunk, whole middle chunk",
|
||||
},
|
||||
{
|
||||
name: "single_byte_each_boundary",
|
||||
rangeStart: chunkSize - 1, // 1 byte from chunk 1
|
||||
rangeEnd: 2 * chunkSize, // 1 byte from chunk 3
|
||||
desc: "1 byte from chunk 1, entire chunk 2, 1 byte from chunk 3",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Logf("Test: %s", tc.desc)
|
||||
t.Logf("Range: bytes=%d-%d (spanning bytes at offsets across chunk boundaries)", tc.rangeStart, tc.rangeEnd)
|
||||
|
||||
result, err := svc.GetObject(&s3.GetObjectInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Key: aws.String(key),
|
||||
Range: aws.String(fmt.Sprintf("bytes=%d-%d", tc.rangeStart, tc.rangeEnd)),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Range GetObject failed: %v", err)
|
||||
}
|
||||
defer result.Body.Close()
|
||||
|
||||
rangeData, err := io.ReadAll(result.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read range response: %v", err)
|
||||
}
|
||||
|
||||
expectedLen := tc.rangeEnd - tc.rangeStart + 1
|
||||
if int64(len(rangeData)) != expectedLen {
|
||||
t.Errorf("Size mismatch: expected %d bytes, got %d", expectedLen, len(rangeData))
|
||||
}
|
||||
|
||||
expectedRange := largeContent[tc.rangeStart : tc.rangeEnd+1]
|
||||
if !bytes.Equal(rangeData, expectedRange) {
|
||||
t.Errorf("Content mismatch in multi-chunk range read")
|
||||
// Find first mismatch for debugging
|
||||
for i := 0; i < len(rangeData) && i < len(expectedRange); i++ {
|
||||
if rangeData[i] != expectedRange[i] {
|
||||
globalOffset := tc.rangeStart + int64(i)
|
||||
chunkNum := globalOffset / chunkSize
|
||||
offsetInChunk := globalOffset % chunkSize
|
||||
t.Errorf("First mismatch at byte %d (chunk %d, offset %d in chunk): expected %02x, got %02x",
|
||||
i, chunkNum, offsetInChunk, expectedRange[i], rangeData[i])
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
t.Logf("✓ Successfully read %d bytes spanning chunks (offsets %d-%d)", len(rangeData), tc.rangeStart, tc.rangeEnd)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestS3VolumeEncryptionCopy tests that copying encrypted objects works correctly.
|
||||
func TestS3VolumeEncryptionCopy(t *testing.T) {
|
||||
svc := getS3Client(t)
|
||||
bucket := fmt.Sprintf("volume-encryption-copy-%d", time.Now().Unix())
|
||||
|
||||
// Create bucket
|
||||
_, err := svc.CreateBucket(&s3.CreateBucketInput{
|
||||
Bucket: aws.String(bucket),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create bucket: %v", err)
|
||||
}
|
||||
defer cleanupBucket(t, svc, bucket)
|
||||
|
||||
srcKey := "source-object.txt"
|
||||
dstKey := "copied-object.txt"
|
||||
content := "Content to be copied with volume-level encryption"
|
||||
|
||||
// Upload source
|
||||
_, err = svc.PutObject(&s3.PutObjectInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Key: aws.String(srcKey),
|
||||
Body: strings.NewReader(content),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("PutObject failed: %v", err)
|
||||
}
|
||||
|
||||
// Copy object
|
||||
_, err = svc.CopyObject(&s3.CopyObjectInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Key: aws.String(dstKey),
|
||||
CopySource: aws.String(fmt.Sprintf("%s/%s", bucket, srcKey)),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CopyObject failed: %v", err)
|
||||
}
|
||||
|
||||
// Read copied object
|
||||
result, err := svc.GetObject(&s3.GetObjectInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Key: aws.String(dstKey),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("GetObject failed for copied object: %v", err)
|
||||
}
|
||||
defer result.Body.Close()
|
||||
|
||||
data, err := io.ReadAll(result.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read copied object: %v", err)
|
||||
}
|
||||
|
||||
if string(data) != content {
|
||||
t.Errorf("Copied content mismatch:\n expected: %q\n got: %q", content, string(data))
|
||||
} else {
|
||||
t.Logf("Successfully copied encrypted object")
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
func getS3Client(t *testing.T) *s3.S3 {
|
||||
// Use credentials that match the Makefile configuration
|
||||
// ACCESS_KEY ?= some_access_key1
|
||||
// SECRET_KEY ?= some_secret_key1
|
||||
sess, err := session.NewSession(&aws.Config{
|
||||
Region: aws.String("us-east-1"),
|
||||
Endpoint: aws.String("http://localhost:8333"),
|
||||
DisableSSL: aws.Bool(true),
|
||||
S3ForcePathStyle: aws.Bool(true),
|
||||
Credentials: credentials.NewStaticCredentials("some_access_key1", "some_secret_key1", ""),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create session: %v", err)
|
||||
}
|
||||
return s3.New(sess)
|
||||
}
|
||||
|
||||
func cleanupBucket(t *testing.T, svc *s3.S3, bucket string) {
|
||||
ctx := context.Background()
|
||||
_ = ctx
|
||||
|
||||
// List and delete all objects
|
||||
listResult, err := svc.ListObjectsV2(&s3.ListObjectsV2Input{
|
||||
Bucket: aws.String(bucket),
|
||||
})
|
||||
if err != nil {
|
||||
t.Logf("Warning: failed to list objects for cleanup: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, obj := range listResult.Contents {
|
||||
_, err := svc.DeleteObject(&s3.DeleteObjectInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Key: obj.Key,
|
||||
})
|
||||
if err != nil {
|
||||
t.Logf("Warning: failed to delete object %s: %v", *obj.Key, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Delete bucket
|
||||
_, err = svc.DeleteBucket(&s3.DeleteBucketInput{
|
||||
Bucket: aws.String(bucket),
|
||||
})
|
||||
if err != nil {
|
||||
t.Logf("Warning: failed to delete bucket %s: %v", bucket, err)
|
||||
}
|
||||
}
|
||||
@@ -72,17 +72,41 @@ start-server: check-deps
|
||||
fi
|
||||
@echo "🔍 DEBUG: Checking binary at $(WEED_BINARY)"
|
||||
@ls -la $(WEED_BINARY) || (echo "❌ Binary not found!" && exit 1)
|
||||
@echo "🔍 DEBUG: Checking config file at ../../../docker/compose/s3.json"
|
||||
@ls -la ../../../docker/compose/s3.json || echo "⚠️ Config file not found, continuing without it"
|
||||
@echo "🔍 DEBUG: Creating volume directory..."
|
||||
@mkdir -p ./test-volume-data
|
||||
@echo "🔍 DEBUG: Launching SeaweedFS server in background..."
|
||||
@echo "🔍 DEBUG: Command: $(WEED_BINARY) mini -dir=./test-volume-data -s3.port=$(S3_PORT)"
|
||||
@$(WEED_BINARY) mini \
|
||||
@echo "🔍 DEBUG: Command: $(WEED_BINARY) server -filer -filer.maxMB=64 -s3 -ip.bind 0.0.0.0 -dir=./test-volume-data -master.raftHashicorp -master.electionTimeout 1s -master.volumeSizeLimitMB=100 -volume.max=100 -volume.preStopSeconds=1 -master.port=$(MASTER_PORT) -volume.port=$(VOLUME_PORT) -filer.port=$(FILER_PORT) -s3.port=$(S3_PORT) -metricsPort=9329 -s3.allowDeleteBucketNotEmpty=true -s3.config=../../../docker/compose/s3.json -master.peers=none"
|
||||
@$(WEED_BINARY) server \
|
||||
-filer \
|
||||
-filer.maxMB=64 \
|
||||
-s3 \
|
||||
-ip.bind 0.0.0.0 \
|
||||
-dir=./test-volume-data \
|
||||
-master.raftHashicorp \
|
||||
-master.electionTimeout 1s \
|
||||
-master.volumeSizeLimitMB=100 \
|
||||
-volume.max=100 \
|
||||
-volume.preStopSeconds=1 \
|
||||
-master.port=$(MASTER_PORT) \
|
||||
-volume.port=$(VOLUME_PORT) \
|
||||
-filer.port=$(FILER_PORT) \
|
||||
-s3.port=$(S3_PORT) \
|
||||
> weed-test.log 2>&1 & \
|
||||
echo $$! > weed-server.pid
|
||||
-metricsPort=9329 \
|
||||
-s3.allowDeleteBucketNotEmpty=true \
|
||||
-s3.config=../../../docker/compose/s3.json \
|
||||
-master.peers=none \
|
||||
> weed-test.log 2>&1 & echo $$! > weed-server.pid
|
||||
@echo "🔍 DEBUG: Server PID: $$(cat weed-server.pid 2>/dev/null || echo 'PID file not found')"
|
||||
@echo "🔍 DEBUG: Checking if PID is still running..."
|
||||
@sleep 2
|
||||
@if [ -f weed-server.pid ]; then \
|
||||
SERVER_PID=$$(cat weed-server.pid); \
|
||||
ps -p $$SERVER_PID || echo "⚠️ Server PID $$SERVER_PID not found after 2 seconds"; \
|
||||
else \
|
||||
echo "⚠️ PID file not found"; \
|
||||
fi
|
||||
@echo "🔍 DEBUG: Waiting for server to start (up to 90 seconds)..."
|
||||
@for i in $$(seq 1 90); do \
|
||||
echo "🔍 DEBUG: Attempt $$i/90 - checking port $(S3_PORT)"; \
|
||||
@@ -101,6 +125,8 @@ start-server: check-deps
|
||||
if [ $$i -eq 15 ]; then \
|
||||
echo "🔍 DEBUG: After 15 seconds, checking port bindings..."; \
|
||||
netstat -tlnp 2>/dev/null | grep $(S3_PORT) || echo "Port $(S3_PORT) not bound"; \
|
||||
netstat -tlnp 2>/dev/null | grep $(MASTER_PORT) || echo "Port $(MASTER_PORT) not bound"; \
|
||||
netstat -tlnp 2>/dev/null | grep $(VOLUME_PORT) || echo "Port $(VOLUME_PORT) not bound"; \
|
||||
fi; \
|
||||
if [ $$i -eq 30 ]; then \
|
||||
echo "⚠️ Server taking longer than expected (30s), checking logs..."; \
|
||||
@@ -115,7 +141,7 @@ start-server: check-deps
|
||||
echo "🔍 DEBUG: Final process check:"; \
|
||||
ps aux | grep weed | grep -v grep || echo "No weed processes found"; \
|
||||
echo "🔍 DEBUG: Final port check:"; \
|
||||
netstat -tlnp 2>/dev/null | grep -E "($(S3_PORT))" || echo "No ports bound"; \
|
||||
netstat -tlnp 2>/dev/null | grep -E "($(S3_PORT)|$(MASTER_PORT)|$(VOLUME_PORT))" || echo "No ports bound"; \
|
||||
echo "=== Full server logs ==="; \
|
||||
if [ -f weed-test.log ]; then \
|
||||
cat weed-test.log; \
|
||||
|
||||
@@ -63,12 +63,18 @@ func getS3Client(t *testing.T) *s3.Client {
|
||||
defaultConfig.SecretKey,
|
||||
"",
|
||||
)),
|
||||
config.WithEndpointResolverWithOptions(aws.EndpointResolverWithOptionsFunc(
|
||||
func(service, region string, options ...interface{}) (aws.Endpoint, error) {
|
||||
return aws.Endpoint{
|
||||
URL: defaultConfig.Endpoint,
|
||||
SigningRegion: defaultConfig.Region,
|
||||
}, nil
|
||||
})),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
client := s3.NewFromConfig(cfg, func(o *s3.Options) {
|
||||
o.UsePathStyle = true
|
||||
o.BaseEndpoint = aws.String(defaultConfig.Endpoint)
|
||||
})
|
||||
return client
|
||||
}
|
||||
@@ -107,33 +113,6 @@ func cleanupTestBucket(t *testing.T, client *s3.Client, bucketName string) {
|
||||
}
|
||||
}
|
||||
|
||||
// Delete all versions and delete markers if versioning is enabled
|
||||
listVersionsResp, err := client.ListObjectVersions(context.TODO(), &s3.ListObjectVersionsInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
})
|
||||
if err == nil {
|
||||
for _, version := range listVersionsResp.Versions {
|
||||
_, err := client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: version.Key,
|
||||
VersionId: version.VersionId,
|
||||
})
|
||||
if err != nil {
|
||||
t.Logf("Warning: failed to delete version %s: %v", *version.Key, err)
|
||||
}
|
||||
}
|
||||
for _, marker := range listVersionsResp.DeleteMarkers {
|
||||
_, err := client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: marker.Key,
|
||||
VersionId: marker.VersionId,
|
||||
})
|
||||
if err != nil {
|
||||
t.Logf("Warning: failed to delete marker %s: %v", *marker.Key, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Then delete the bucket
|
||||
_, err = client.DeleteBucket(context.TODO(), &s3.DeleteBucketInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
|
||||
@@ -1,434 +0,0 @@
|
||||
package tagging
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// This is the fix for GitHub issue #7868 where tagging failed with "no entry is found in filer store"
|
||||
// TestPutObjectTaggingOnVersionedBucket tests setting tags on objects in a versioned bucket
|
||||
func TestPutObjectTaggingOnVersionedBucket(t *testing.T) {
|
||||
client := getS3Client(t)
|
||||
bucketName := createVersionedTestBucket(t, client)
|
||||
defer cleanupTestBucket(t, client, bucketName)
|
||||
|
||||
// Put object in versioned bucket
|
||||
objectKey := "versioned-object-with-tags"
|
||||
objectContent := "Hello, Versioned World!"
|
||||
_, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
|
||||
Body: strings.NewReader(objectContent),
|
||||
Key: aws.String(objectKey),
|
||||
Bucket: aws.String(bucketName),
|
||||
})
|
||||
require.NoError(t, err, "Should be able to put object in versioned bucket")
|
||||
|
||||
// Set tags on the object in versioned bucket
|
||||
_, err = client.PutObjectTagging(context.TODO(), &s3.PutObjectTaggingInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
Tagging: &types.Tagging{
|
||||
TagSet: []types.Tag{
|
||||
{
|
||||
Key: aws.String("env"),
|
||||
Value: aws.String("production"),
|
||||
},
|
||||
{
|
||||
Key: aws.String("team"),
|
||||
Value: aws.String("platform"),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err, "Should be able to put tags on versioned object")
|
||||
|
||||
// Get the tags back
|
||||
tagResp, err := client.GetObjectTagging(context.TODO(), &s3.GetObjectTaggingInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
})
|
||||
require.NoError(t, err, "Should be able to get tags from versioned object")
|
||||
|
||||
// Verify tags
|
||||
assert.Len(t, tagResp.TagSet, 2, "Should have 2 tags")
|
||||
tagMap := make(map[string]string)
|
||||
for _, tag := range tagResp.TagSet {
|
||||
tagMap[*tag.Key] = *tag.Value
|
||||
}
|
||||
assert.Equal(t, "production", tagMap["env"], "env tag should be 'production'")
|
||||
assert.Equal(t, "platform", tagMap["team"], "team tag should be 'platform'")
|
||||
}
|
||||
|
||||
// TestPutObjectTaggingOnSpecificVersionInVersionedBucket tests setting tags on a specific version
|
||||
func TestPutObjectTaggingOnSpecificVersionInVersionedBucket(t *testing.T) {
|
||||
client := getS3Client(t)
|
||||
bucketName := createVersionedTestBucket(t, client)
|
||||
defer cleanupTestBucket(t, client, bucketName)
|
||||
|
||||
// Create multiple versions of the object
|
||||
objectKey := "multi-version-object"
|
||||
version1Resp, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
|
||||
Body: strings.NewReader("Version 1"),
|
||||
Key: aws.String(objectKey),
|
||||
Bucket: aws.String(bucketName),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Small delay to ensure different version IDs
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
versionId1 := *version1Resp.VersionId
|
||||
|
||||
version2Resp, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
|
||||
Body: strings.NewReader("Version 2"),
|
||||
Key: aws.String(objectKey),
|
||||
Bucket: aws.String(bucketName),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
versionId2 := *version2Resp.VersionId
|
||||
|
||||
// Set tags on version 1
|
||||
_, err = client.PutObjectTagging(context.TODO(), &s3.PutObjectTaggingInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
VersionId: aws.String(versionId1),
|
||||
Tagging: &types.Tagging{
|
||||
TagSet: []types.Tag{
|
||||
{
|
||||
Key: aws.String("version"),
|
||||
Value: aws.String("v1"),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err, "Should be able to put tags on specific version 1")
|
||||
|
||||
// Set tags on version 2
|
||||
_, err = client.PutObjectTagging(context.TODO(), &s3.PutObjectTaggingInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
VersionId: aws.String(versionId2),
|
||||
Tagging: &types.Tagging{
|
||||
TagSet: []types.Tag{
|
||||
{
|
||||
Key: aws.String("version"),
|
||||
Value: aws.String("v2"),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err, "Should be able to put tags on specific version 2")
|
||||
|
||||
// Get tags from version 1
|
||||
tagResp1, err := client.GetObjectTagging(context.TODO(), &s3.GetObjectTaggingInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
VersionId: aws.String(versionId1),
|
||||
})
|
||||
require.NoError(t, err, "Should be able to get tags from version 1")
|
||||
assert.Len(t, tagResp1.TagSet, 1, "Version 1 should have 1 tag")
|
||||
assert.Equal(t, "v1", *tagResp1.TagSet[0].Value, "Version 1 tag value should be 'v1'")
|
||||
|
||||
// Get tags from version 2
|
||||
tagResp2, err := client.GetObjectTagging(context.TODO(), &s3.GetObjectTaggingInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
VersionId: aws.String(versionId2),
|
||||
})
|
||||
require.NoError(t, err, "Should be able to get tags from version 2")
|
||||
assert.Len(t, tagResp2.TagSet, 1, "Version 2 should have 1 tag")
|
||||
assert.Equal(t, "v2", *tagResp2.TagSet[0].Value, "Version 2 tag value should be 'v2'")
|
||||
}
|
||||
|
||||
// TestDeleteObjectTaggingOnVersionedBucket tests deleting tags from versioned objects
|
||||
func TestDeleteObjectTaggingOnVersionedBucket(t *testing.T) {
|
||||
client := getS3Client(t)
|
||||
bucketName := createVersionedTestBucket(t, client)
|
||||
defer cleanupTestBucket(t, client, bucketName)
|
||||
|
||||
// Put object with tags in versioned bucket
|
||||
objectKey := "versioned-object-tag-delete"
|
||||
objectContent := "Hello, Delete Tags!"
|
||||
_, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
|
||||
Body: strings.NewReader(objectContent),
|
||||
Key: aws.String(objectKey),
|
||||
Bucket: aws.String(bucketName),
|
||||
Tagging: aws.String("env=dev&purpose=testing"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify tags exist
|
||||
tagResp, err := client.GetObjectTagging(context.TODO(), &s3.GetObjectTaggingInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, tagResp.TagSet, 2, "Should have 2 tags before deletion")
|
||||
|
||||
// Delete tags from the versioned object
|
||||
_, err = client.DeleteObjectTagging(context.TODO(), &s3.DeleteObjectTaggingInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
})
|
||||
require.NoError(t, err, "Should be able to delete tags from versioned object")
|
||||
|
||||
// Verify tags are deleted
|
||||
tagResp, err = client.GetObjectTagging(context.TODO(), &s3.GetObjectTaggingInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, tagResp.TagSet, 0, "Should have 0 tags after deletion")
|
||||
}
|
||||
|
||||
// TestDeleteObjectTaggingOnSpecificVersionInVersionedBucket tests deleting tags on a specific version
|
||||
func TestDeleteObjectTaggingOnSpecificVersionInVersionedBucket(t *testing.T) {
|
||||
client := getS3Client(t)
|
||||
bucketName := createVersionedTestBucket(t, client)
|
||||
defer cleanupTestBucket(t, client, bucketName)
|
||||
|
||||
// Create two versions with tags
|
||||
objectKey := "versioned-multi-delete-tags"
|
||||
version1Resp, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
|
||||
Body: strings.NewReader("Version 1"),
|
||||
Key: aws.String(objectKey),
|
||||
Bucket: aws.String(bucketName),
|
||||
Tagging: aws.String("version=v1&keep=true"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Small delay to ensure different version IDs
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
versionId1 := *version1Resp.VersionId
|
||||
|
||||
version2Resp, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
|
||||
Body: strings.NewReader("Version 2"),
|
||||
Key: aws.String(objectKey),
|
||||
Bucket: aws.String(bucketName),
|
||||
Tagging: aws.String("version=v2&keep=true"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
versionId2 := *version2Resp.VersionId
|
||||
|
||||
// Delete tags from version 1 only
|
||||
_, err = client.DeleteObjectTagging(context.TODO(), &s3.DeleteObjectTaggingInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
VersionId: aws.String(versionId1),
|
||||
})
|
||||
require.NoError(t, err, "Should be able to delete tags from version 1")
|
||||
|
||||
// Verify version 1 has no tags
|
||||
tagResp1, err := client.GetObjectTagging(context.TODO(), &s3.GetObjectTaggingInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
VersionId: aws.String(versionId1),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, tagResp1.TagSet, 0, "Version 1 should have 0 tags after deletion")
|
||||
|
||||
// Verify version 2 still has tags
|
||||
tagResp2, err := client.GetObjectTagging(context.TODO(), &s3.GetObjectTaggingInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
VersionId: aws.String(versionId2),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, tagResp2.TagSet, 2, "Version 2 should still have 2 tags")
|
||||
}
|
||||
|
||||
// TestGetObjectTaggingOnVersionedBucket tests retrieving tags from versioned objects
|
||||
func TestGetObjectTaggingOnVersionedBucket(t *testing.T) {
|
||||
client := getS3Client(t)
|
||||
bucketName := createVersionedTestBucket(t, client)
|
||||
defer cleanupTestBucket(t, client, bucketName)
|
||||
|
||||
// Create multiple versions
|
||||
objectKey := "versioned-object-get-tags"
|
||||
version1Resp, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
|
||||
Body: strings.NewReader("Version 1"),
|
||||
Key: aws.String(objectKey),
|
||||
Bucket: aws.String(bucketName),
|
||||
Tagging: aws.String("v=1&stage=dev"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
versionId1 := *version1Resp.VersionId
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
version2Resp, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
|
||||
Body: strings.NewReader("Version 2"),
|
||||
Key: aws.String(objectKey),
|
||||
Bucket: aws.String(bucketName),
|
||||
Tagging: aws.String("v=2&stage=prod"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
versionId2 := *version2Resp.VersionId
|
||||
|
||||
// Get tags from specific versions
|
||||
tagResp1, err := client.GetObjectTagging(context.TODO(), &s3.GetObjectTaggingInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
VersionId: aws.String(versionId1),
|
||||
})
|
||||
require.NoError(t, err, "Should be able to get tags from version 1")
|
||||
assert.Len(t, tagResp1.TagSet, 2, "Version 1 should have 2 tags")
|
||||
tagMap1 := make(map[string]string)
|
||||
for _, tag := range tagResp1.TagSet {
|
||||
tagMap1[*tag.Key] = *tag.Value
|
||||
}
|
||||
assert.Equal(t, "1", tagMap1["v"], "Version 1 should have v=1")
|
||||
assert.Equal(t, "dev", tagMap1["stage"], "Version 1 should have stage=dev")
|
||||
|
||||
tagResp2, err := client.GetObjectTagging(context.TODO(), &s3.GetObjectTaggingInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
VersionId: aws.String(versionId2),
|
||||
})
|
||||
require.NoError(t, err, "Should be able to get tags from version 2")
|
||||
assert.Len(t, tagResp2.TagSet, 2, "Version 2 should have 2 tags")
|
||||
tagMap2 := make(map[string]string)
|
||||
for _, tag := range tagResp2.TagSet {
|
||||
tagMap2[*tag.Key] = *tag.Value
|
||||
}
|
||||
assert.Equal(t, "2", tagMap2["v"], "Version 2 should have v=2")
|
||||
assert.Equal(t, "prod", tagMap2["stage"], "Version 2 should have stage=prod")
|
||||
|
||||
// Get tags from latest version (should be version 2)
|
||||
tagRespLatest, err := client.GetObjectTagging(context.TODO(), &s3.GetObjectTaggingInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
})
|
||||
require.NoError(t, err, "Should be able to get tags from latest version")
|
||||
assert.Len(t, tagRespLatest.TagSet, 2, "Latest version should have 2 tags")
|
||||
tagMapLatest := make(map[string]string)
|
||||
for _, tag := range tagRespLatest.TagSet {
|
||||
tagMapLatest[*tag.Key] = *tag.Value
|
||||
}
|
||||
assert.Equal(t, "2", tagMapLatest["v"], "Latest version should have v=2")
|
||||
assert.Equal(t, "prod", tagMapLatest["stage"], "Latest version should have stage=prod")
|
||||
}
|
||||
|
||||
// TestModifyTagsOnVersionedObject tests changing tags on different versions independently
|
||||
func TestModifyTagsOnVersionedObject(t *testing.T) {
|
||||
client := getS3Client(t)
|
||||
bucketName := createVersionedTestBucket(t, client)
|
||||
defer cleanupTestBucket(t, client, bucketName)
|
||||
|
||||
// Create version 1
|
||||
objectKey := "versioned-modify-tags"
|
||||
version1Resp, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
|
||||
Body: strings.NewReader("Version 1"),
|
||||
Key: aws.String(objectKey),
|
||||
Bucket: aws.String(bucketName),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
versionId1 := *version1Resp.VersionId
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
// Create version 2
|
||||
version2Resp, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
|
||||
Body: strings.NewReader("Version 2"),
|
||||
Key: aws.String(objectKey),
|
||||
Bucket: aws.String(bucketName),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
versionId2 := *version2Resp.VersionId
|
||||
|
||||
// Add tags to version 1
|
||||
_, err = client.PutObjectTagging(context.TODO(), &s3.PutObjectTaggingInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
VersionId: aws.String(versionId1),
|
||||
Tagging: &types.Tagging{
|
||||
TagSet: []types.Tag{
|
||||
{
|
||||
Key: aws.String("status"),
|
||||
Value: aws.String("old"),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Modify tags on version 1 (replace status and add new tag)
|
||||
_, err = client.PutObjectTagging(context.TODO(), &s3.PutObjectTaggingInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
VersionId: aws.String(versionId1),
|
||||
Tagging: &types.Tagging{
|
||||
TagSet: []types.Tag{
|
||||
{
|
||||
Key: aws.String("status"),
|
||||
Value: aws.String("archived"),
|
||||
},
|
||||
{
|
||||
Key: aws.String("archived-date"),
|
||||
Value: aws.String("2024-01-01"),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Add tags to version 2
|
||||
_, err = client.PutObjectTagging(context.TODO(), &s3.PutObjectTaggingInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
VersionId: aws.String(versionId2),
|
||||
Tagging: &types.Tagging{
|
||||
TagSet: []types.Tag{
|
||||
{
|
||||
Key: aws.String("status"),
|
||||
Value: aws.String("current"),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify final state
|
||||
tagResp1, err := client.GetObjectTagging(context.TODO(), &s3.GetObjectTaggingInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
VersionId: aws.String(versionId1),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, tagResp1.TagSet, 2, "Version 1 should have 2 tags after modification")
|
||||
|
||||
tagResp2, err := client.GetObjectTagging(context.TODO(), &s3.GetObjectTaggingInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
VersionId: aws.String(versionId2),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, tagResp2.TagSet, 1, "Version 2 should have 1 tag")
|
||||
}
|
||||
|
||||
// createVersionedTestBucket creates a test bucket with versioning enabled
|
||||
func createVersionedTestBucket(t *testing.T, client *s3.Client) string {
|
||||
bucketName := createTestBucket(t, client)
|
||||
|
||||
// Enable versioning on the bucket
|
||||
_, err := client.PutBucketVersioning(context.TODO(), &s3.PutBucketVersioningInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
VersioningConfiguration: &types.VersioningConfiguration{
|
||||
Status: types.BucketVersioningStatusEnabled,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err, "Should be able to enable versioning")
|
||||
|
||||
// Wait for versioning configuration to be applied
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
return bucketName
|
||||
}
|
||||
+41
-26
@@ -43,13 +43,13 @@ build-weed:
|
||||
@echo "Building SeaweedFS binary..."
|
||||
@cd ../../../weed && go build -o weed_binary .
|
||||
@chmod +x $(WEED_BINARY)
|
||||
@echo "OK SeaweedFS binary built at $(WEED_BINARY)"
|
||||
@echo "✅ SeaweedFS binary built at $(WEED_BINARY)"
|
||||
|
||||
check-deps: build-weed
|
||||
@echo "Checking dependencies..."
|
||||
@echo "DEBUG: Checking Go installation..."
|
||||
@echo "🔍 DEBUG: Checking Go installation..."
|
||||
@command -v go >/dev/null 2>&1 || (echo "Go is required but not installed" && exit 1)
|
||||
@echo "DEBUG: Go version: $$(go version)"
|
||||
@echo "🔍 DEBUG: Go version: $$(go version)"
|
||||
@echo "🔍 DEBUG: Checking binary at $(WEED_BINARY)..."
|
||||
@test -f $(WEED_BINARY) || (echo "SeaweedFS binary not found at $(WEED_BINARY)" && exit 1)
|
||||
@echo "🔍 DEBUG: Binary size: $$(ls -lh $(WEED_BINARY) | awk '{print $$5}')"
|
||||
@@ -57,7 +57,7 @@ check-deps: build-weed
|
||||
@echo "🔍 DEBUG: Checking Go module dependencies..."
|
||||
@go list -m github.com/aws/aws-sdk-go-v2 >/dev/null 2>&1 || (echo "AWS SDK Go v2 not found. Run 'go mod tidy'." && exit 1)
|
||||
@go list -m github.com/stretchr/testify >/dev/null 2>&1 || (echo "Testify not found. Run 'go mod tidy'." && exit 1)
|
||||
@echo "OK All dependencies are available"
|
||||
@echo "✅ All dependencies are available"
|
||||
|
||||
# Start SeaweedFS server for testing
|
||||
start-server: check-deps
|
||||
@@ -81,11 +81,21 @@ start-server: check-deps
|
||||
@echo "🔍 DEBUG: Creating volume directory..."
|
||||
@mkdir -p ./test-volume-data
|
||||
@echo "🔍 DEBUG: Launching SeaweedFS server in background..."
|
||||
@echo "🔍 DEBUG: Command: $(WEED_BINARY) mini -dir=./test-volume-data -s3.port=$(S3_PORT) -s3.config=../../../docker/compose/s3.json"
|
||||
@$(WEED_BINARY) mini \
|
||||
-dir=./test-volume-data \
|
||||
@echo "🔍 DEBUG: Command: $(WEED_BINARY) server -debug -s3 -s3.port=$(S3_PORT) -s3.allowDeleteBucketNotEmpty=true -s3.config=../../../docker/compose/s3.json -filer -filer.maxMB=64 -master.volumeSizeLimitMB=50 -master.peers=none -volume.max=100 -dir=./test-volume-data -volume.preStopSeconds=1 -metricsPort=9324"
|
||||
@$(WEED_BINARY) server \
|
||||
-debug \
|
||||
-s3 \
|
||||
-s3.port=$(S3_PORT) \
|
||||
-s3.allowDeleteBucketNotEmpty=true \
|
||||
-s3.config=../../../docker/compose/s3.json \
|
||||
-filer \
|
||||
-filer.maxMB=64 \
|
||||
-master.volumeSizeLimitMB=50 \
|
||||
-master.peers=none \
|
||||
-volume.max=100 \
|
||||
-dir=./test-volume-data \
|
||||
-volume.preStopSeconds=1 \
|
||||
-metricsPort=9324 \
|
||||
> weed-test.log 2>&1 & echo $$! > weed-server.pid
|
||||
@echo "🔍 DEBUG: Server PID: $$(cat weed-server.pid 2>/dev/null || echo 'PID file not found')"
|
||||
@echo "🔍 DEBUG: Checking if PID is still running..."
|
||||
@@ -212,13 +222,13 @@ test-with-server: start-server
|
||||
test-versioning-with-configs: check-deps
|
||||
@echo "Testing with different S3 configurations..."
|
||||
@echo "Testing with empty folder allowed..."
|
||||
@$(WEED_BINARY) mini -s3.port=$(S3_PORT) > weed-test-config1.log 2>&1 & echo $$! > weed-config1.pid
|
||||
@$(WEED_BINARY) server -s3 -s3.port=$(S3_PORT) -filer -master.volumeSizeLimitMB=100 -master.peers=none -volume.max=100 > weed-test-config1.log 2>&1 & echo $$! > weed-config1.pid
|
||||
@sleep 5
|
||||
@go test -v -timeout=5m -run "TestVersioningBasicWorkflow" . || true
|
||||
@if [ -f weed-config1.pid ]; then kill -TERM $$(cat weed-config1.pid) 2>/dev/null || true; rm -f weed-config1.pid; fi
|
||||
@sleep 2
|
||||
@echo "Testing with delete bucket not empty disabled..."
|
||||
@$(WEED_BINARY) mini -s3.port=$(S3_PORT) -s3.allowDeleteBucketNotEmpty=false > weed-test-config2.log 2>&1 & echo $$! > weed-config2.pid
|
||||
@$(WEED_BINARY) server -s3 -s3.port=$(S3_PORT) -s3.allowDeleteBucketNotEmpty=false -filer -master.volumeSizeLimitMB=100 -master.peers=none -volume.max=100 > weed-test-config2.log 2>&1 & echo $$! > weed-config2.pid
|
||||
@sleep 5
|
||||
@go test -v -timeout=5m -run "TestVersioningBasicWorkflow" . || true
|
||||
@if [ -f weed-config2.pid ]; then kill -TERM $$(cat weed-config2.pid) 2>/dev/null || true; rm -f weed-config2.pid; fi
|
||||
@@ -261,14 +271,19 @@ debug-server:
|
||||
@echo "Starting SeaweedFS server in debug mode..."
|
||||
@$(MAKE) stop-server
|
||||
@mkdir -p ./test-volume-data
|
||||
@$(WEED_BINARY) mini \
|
||||
@$(WEED_BINARY) server \
|
||||
-debug \
|
||||
-s3 \
|
||||
-s3.port=$(S3_PORT) \
|
||||
-s3.allowDeleteBucketNotEmpty=true \
|
||||
-s3.config=../../../docker/compose/s3.json \
|
||||
-filer \
|
||||
-filer.maxMB=16 \
|
||||
-master.volumeSizeLimitMB=50 \
|
||||
-master.peers=none \
|
||||
-volume.max=100 \
|
||||
-dir=./test-volume-data \
|
||||
-volume.preStopSeconds=1 \
|
||||
-metricsPort=9324
|
||||
|
||||
# Run a single test for debugging
|
||||
@@ -305,24 +320,24 @@ health-check:
|
||||
# Simple server start without process cleanup (for CI troubleshooting)
|
||||
start-server-simple: check-deps
|
||||
@echo "Starting SeaweedFS server (simple mode)..."
|
||||
@$(WEED_BINARY) mini \
|
||||
-dir=$(SERVER_DIR) \
|
||||
@$(WEED_BINARY) server \
|
||||
-debug \
|
||||
-s3 \
|
||||
-s3.port=$(S3_PORT) \
|
||||
-s3.config=$(S3_CONFIG) \
|
||||
> weed-server.log 2>&1 & \
|
||||
echo $$! > weed-server.pid
|
||||
|
||||
@echo "Waiting for S3 server to be ready..."
|
||||
@for i in $$(seq 1 30); do \
|
||||
if echo | nc -z localhost $(S3_PORT); then \
|
||||
echo "S3 server is ready!"; \
|
||||
exit 0; \
|
||||
fi; \
|
||||
sleep 1; \
|
||||
done; \
|
||||
echo "S3 server failed to start"; \
|
||||
exit 1
|
||||
-s3.allowDeleteBucketNotEmpty=true \
|
||||
-s3.config=../../../docker/compose/s3.json \
|
||||
-filer \
|
||||
-filer.maxMB=64 \
|
||||
-master.volumeSizeLimitMB=50 \
|
||||
-master.peers=none \
|
||||
-volume.max=100 \
|
||||
-volume.preStopSeconds=1 \
|
||||
-metricsPort=9324 \
|
||||
> weed-test.log 2>&1 & echo $$! > weed-server.pid
|
||||
@echo "Server PID: $$(cat weed-server.pid)"
|
||||
@echo "Waiting for server to start..."
|
||||
@sleep 10
|
||||
@curl -s http://localhost:$(S3_PORT) >/dev/null 2>&1 && echo "✅ Server started successfully" || echo "❌ Server failed to start"
|
||||
|
||||
# Simple test run without server management
|
||||
test-versioning-simple: check-deps
|
||||
|
||||
@@ -1,274 +0,0 @@
|
||||
package s3api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestListObjectVersionsDelimiter tests delimiter functionality in ListObjectVersions
|
||||
func TestListObjectVersionsDelimiter(t *testing.T) {
|
||||
client := getS3Client(t)
|
||||
bucketName := getNewBucketName()
|
||||
|
||||
// Create bucket and enable versioning
|
||||
createBucket(t, client, bucketName)
|
||||
defer deleteBucket(t, client, bucketName)
|
||||
enableVersioning(t, client, bucketName)
|
||||
|
||||
// Create test structure:
|
||||
// - folder1/file1.txt
|
||||
// - folder1/file2.txt
|
||||
// - folder2/file3.txt
|
||||
// - root-file.txt
|
||||
testObjects := []string{
|
||||
"folder1/file1.txt",
|
||||
"folder1/file2.txt",
|
||||
"folder2/file3.txt",
|
||||
"root-file.txt",
|
||||
}
|
||||
|
||||
for _, key := range testObjects {
|
||||
putObject(t, client, bucketName, key, fmt.Sprintf("Content of %s", key))
|
||||
}
|
||||
|
||||
t.Run("Delimiter groups folders correctly", func(t *testing.T) {
|
||||
// List with delimiter='/' and no prefix
|
||||
// Should return: root-file.txt and CommonPrefixes: folder1/, folder2/
|
||||
resp, err := client.ListObjectVersions(context.TODO(), &s3.ListObjectVersionsInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Delimiter: aws.String("/"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Extract keys and prefixes
|
||||
versionKeys := make([]string, 0)
|
||||
for _, v := range resp.Versions {
|
||||
versionKeys = append(versionKeys, *v.Key)
|
||||
}
|
||||
|
||||
prefixValues := make([]string, 0)
|
||||
for _, p := range resp.CommonPrefixes {
|
||||
prefixValues = append(prefixValues, *p.Prefix)
|
||||
}
|
||||
|
||||
// Verify results
|
||||
assert.Contains(t, versionKeys, "root-file.txt", "Should include root-file.txt")
|
||||
assert.Contains(t, prefixValues, "folder1/", "Should include folder1/ prefix")
|
||||
assert.Contains(t, prefixValues, "folder2/", "Should include folder2/ prefix")
|
||||
assert.NotContains(t, versionKeys, "folder1/file1.txt", "folder1/file1.txt should be grouped under folder1/")
|
||||
assert.NotContains(t, versionKeys, "folder2/file3.txt", "folder2/file3.txt should be grouped under folder2/")
|
||||
|
||||
t.Logf("✓ Versions: %v", versionKeys)
|
||||
t.Logf("✓ CommonPrefixes: %v", prefixValues)
|
||||
})
|
||||
|
||||
t.Run("Prefix filtering with delimiter", func(t *testing.T) {
|
||||
// List with delimiter='/' and prefix='folder1/'
|
||||
// Should return: folder1/file1.txt, folder1/file2.txt
|
||||
resp, err := client.ListObjectVersions(context.TODO(), &s3.ListObjectVersionsInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Prefix: aws.String("folder1/"),
|
||||
Delimiter: aws.String("/"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
versionKeys := make([]string, 0)
|
||||
for _, v := range resp.Versions {
|
||||
versionKeys = append(versionKeys, *v.Key)
|
||||
}
|
||||
|
||||
assert.Len(t, versionKeys, 2, "Should have 2 versions")
|
||||
assert.Contains(t, versionKeys, "folder1/file1.txt")
|
||||
assert.Contains(t, versionKeys, "folder1/file2.txt")
|
||||
assert.Empty(t, resp.CommonPrefixes, "Should have no common prefixes")
|
||||
|
||||
t.Logf("✓ Prefix filtering works: %v", versionKeys)
|
||||
})
|
||||
|
||||
t.Run("Without delimiter returns all versions", func(t *testing.T) {
|
||||
// List without delimiter - should return all files
|
||||
resp, err := client.ListObjectVersions(context.TODO(), &s3.ListObjectVersionsInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, resp.Versions, 4, "Should have all 4 versions")
|
||||
assert.Empty(t, resp.CommonPrefixes, "Should have no common prefixes without delimiter")
|
||||
|
||||
t.Logf("✓ Without delimiter returns all %d versions", len(resp.Versions))
|
||||
})
|
||||
}
|
||||
|
||||
// TestListObjectVersionsDelimiterTruncation tests MaxKeys with delimiter
|
||||
func TestListObjectVersionsDelimiterTruncation(t *testing.T) {
|
||||
client := getS3Client(t)
|
||||
bucketName := getNewBucketName()
|
||||
|
||||
// Create bucket and enable versioning
|
||||
createBucket(t, client, bucketName)
|
||||
defer deleteBucket(t, client, bucketName)
|
||||
enableVersioning(t, client, bucketName)
|
||||
|
||||
// Create multiple folders to test truncation
|
||||
for i := 0; i < 5; i++ {
|
||||
key := fmt.Sprintf("folder%d/file.txt", i)
|
||||
putObject(t, client, bucketName, key, fmt.Sprintf("Content %d", i))
|
||||
}
|
||||
// Add a root file
|
||||
putObject(t, client, bucketName, "root.txt", "Root content")
|
||||
|
||||
t.Run("MaxKeys limits total items", func(t *testing.T) {
|
||||
resp, err := client.ListObjectVersions(context.TODO(), &s3.ListObjectVersionsInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Delimiter: aws.String("/"),
|
||||
MaxKeys: aws.Int32(3),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.NotNil(t, resp.IsTruncated, "IsTruncated should not be nil")
|
||||
assert.True(t, *resp.IsTruncated, "Should be truncated")
|
||||
assert.NotNil(t, resp.NextKeyMarker, "Should have NextKeyMarker for pagination")
|
||||
|
||||
count := len(resp.Versions) + len(resp.CommonPrefixes)
|
||||
t.Logf("✓ MaxKeys truncation: %d items (versions: %d, prefixes: %d)",
|
||||
count, len(resp.Versions), len(resp.CommonPrefixes))
|
||||
})
|
||||
|
||||
t.Run("Pagination with delimiter", func(t *testing.T) {
|
||||
// Collect all items through pagination
|
||||
allKeys := make([]string, 0)
|
||||
allPrefixes := make([]string, 0)
|
||||
var keyMarker *string
|
||||
var versionMarker *string
|
||||
|
||||
for {
|
||||
input := &s3.ListObjectVersionsInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Delimiter: aws.String("/"),
|
||||
MaxKeys: aws.Int32(2),
|
||||
}
|
||||
if keyMarker != nil {
|
||||
input.KeyMarker = keyMarker
|
||||
}
|
||||
if versionMarker != nil {
|
||||
input.VersionIdMarker = versionMarker
|
||||
}
|
||||
|
||||
resp, err := client.ListObjectVersions(context.TODO(), input)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Collect versions
|
||||
for _, v := range resp.Versions {
|
||||
allKeys = append(allKeys, *v.Key)
|
||||
}
|
||||
|
||||
// Collect prefixes
|
||||
for _, p := range resp.CommonPrefixes {
|
||||
allPrefixes = append(allPrefixes, *p.Prefix)
|
||||
}
|
||||
|
||||
require.NotNil(t, resp.IsTruncated, "IsTruncated should not be nil")
|
||||
if !*resp.IsTruncated {
|
||||
break
|
||||
}
|
||||
|
||||
keyMarker = resp.NextKeyMarker
|
||||
versionMarker = resp.NextVersionIdMarker
|
||||
}
|
||||
|
||||
// Should have collected all items
|
||||
itemsCount := len(allKeys) + len(allPrefixes)
|
||||
assert.GreaterOrEqual(t, itemsCount, 6, "Should collect all items through pagination")
|
||||
|
||||
t.Logf("✓ Pagination collected %d total items (keys: %d, prefixes: %d)",
|
||||
itemsCount, len(allKeys), len(allPrefixes))
|
||||
})
|
||||
|
||||
t.Run("CommonPrefixes are filtered by keyMarker (exclusive)", func(t *testing.T) {
|
||||
// List with keyMarker that should skip some prefixes
|
||||
// We have folder0/, folder1/, folder2/, folder3/, folder4/
|
||||
// Setting keyMarker to "folder2/" should return folder3/, folder4/ and root.txt (if it's > folder2/)
|
||||
resp, err := client.ListObjectVersions(context.TODO(), &s3.ListObjectVersionsInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Delimiter: aws.String("/"),
|
||||
KeyMarker: aws.String("folder2/"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
prefixValues := make([]string, 0)
|
||||
for _, p := range resp.CommonPrefixes {
|
||||
prefixValues = append(prefixValues, *p.Prefix)
|
||||
}
|
||||
|
||||
assert.NotContains(t, prefixValues, "folder0/", "Should skip folder0/")
|
||||
assert.NotContains(t, prefixValues, "folder1/", "Should skip folder1/")
|
||||
assert.NotContains(t, prefixValues, "folder2/", "Should skip folder2/ (exclusive marker)")
|
||||
assert.Contains(t, prefixValues, "folder3/", "Should include folder3/")
|
||||
assert.Contains(t, prefixValues, "folder4/", "Should include folder4/")
|
||||
|
||||
t.Logf("✓ CommonPrefixes filtered by keyMarker: %v", prefixValues)
|
||||
})
|
||||
}
|
||||
|
||||
// TestListObjectVersionsDelimiterWithMultipleVersions tests delimiter with multiple versions of same object
|
||||
func TestListObjectVersionsDelimiterWithMultipleVersions(t *testing.T) {
|
||||
client := getS3Client(t)
|
||||
bucketName := getNewBucketName()
|
||||
|
||||
// Create bucket and enable versioning
|
||||
createBucket(t, client, bucketName)
|
||||
defer deleteBucket(t, client, bucketName)
|
||||
enableVersioning(t, client, bucketName)
|
||||
|
||||
// Create multiple versions of objects in different folders
|
||||
for i := 1; i <= 3; i++ {
|
||||
putObject(t, client, bucketName, "folder1/file.txt", fmt.Sprintf("Version %d", i))
|
||||
putObject(t, client, bucketName, "folder2/file.txt", fmt.Sprintf("Version %d", i))
|
||||
}
|
||||
|
||||
t.Run("Delimiter groups all versions under prefix", func(t *testing.T) {
|
||||
resp, err := client.ListObjectVersions(context.TODO(), &s3.ListObjectVersionsInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Delimiter: aws.String("/"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should have no versions at root level, only common prefixes
|
||||
assert.Empty(t, resp.Versions, "Should have no versions at root")
|
||||
assert.Len(t, resp.CommonPrefixes, 2, "Should have 2 common prefixes")
|
||||
|
||||
prefixValues := make([]string, 0)
|
||||
for _, p := range resp.CommonPrefixes {
|
||||
prefixValues = append(prefixValues, *p.Prefix)
|
||||
}
|
||||
assert.Contains(t, prefixValues, "folder1/")
|
||||
assert.Contains(t, prefixValues, "folder2/")
|
||||
|
||||
t.Logf("✓ All versions grouped under prefixes: %v", prefixValues)
|
||||
})
|
||||
|
||||
t.Run("Listing within prefix shows all versions", func(t *testing.T) {
|
||||
resp, err := client.ListObjectVersions(context.TODO(), &s3.ListObjectVersionsInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Prefix: aws.String("folder1/"),
|
||||
Delimiter: aws.String("/"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, resp.Versions, 3, "Should have 3 versions of folder1/file.txt")
|
||||
assert.Empty(t, resp.CommonPrefixes, "Should have no common prefixes")
|
||||
|
||||
// Verify all versions are for the same key
|
||||
for _, v := range resp.Versions {
|
||||
assert.Equal(t, "folder1/file.txt", *v.Key)
|
||||
}
|
||||
|
||||
t.Logf("✓ Found %d versions within prefix", len(resp.Versions))
|
||||
})
|
||||
}
|
||||
@@ -518,3 +518,4 @@ func TestMultipartUploadDeleteMarkerListBehavior(t *testing.T) {
|
||||
|
||||
t.Logf("Object restored after delete marker removal, ETag=%s", multipartETag)
|
||||
}
|
||||
|
||||
|
||||
@@ -319,3 +319,4 @@ func listAllVersions(t *testing.T, client *s3.Client, bucketName, objectKey stri
|
||||
t.Logf("Total: %d versions in %d pages", len(allVersions), pageCount)
|
||||
return allVersions
|
||||
}
|
||||
|
||||
|
||||
@@ -540,11 +540,11 @@ func TestPathEdgeCases(t *testing.T) {
|
||||
// Therefore, we cannot trigger the server-side path traversal block with this client.
|
||||
// Instead, we verify that the file is created successfully within the jail (contained).
|
||||
// The server-side protection logic is verified in unit tests (sftpd/sftp_server_test.go).
|
||||
|
||||
|
||||
file, err := sftpClient.Create(traversalPath)
|
||||
require.NoError(t, err, "creation should succeed because client sanitizes path")
|
||||
file.Close()
|
||||
|
||||
|
||||
// Clean up
|
||||
err = sftpClient.Remove(traversalPath)
|
||||
require.NoError(t, err)
|
||||
@@ -649,3 +649,4 @@ func TestFileContent(t *testing.T) {
|
||||
sftpClient.Remove(filename)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -420,3 +420,4 @@ func findTestDataPath() string {
|
||||
|
||||
return "./testdata"
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,6 @@ import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
@@ -15,6 +11,9 @@ import (
|
||||
util_http "github.com/seaweedfs/seaweedfs/weed/util/http"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
|
||||
# Makefile for managing local Nginx instance
|
||||
|
||||
# Current directory
|
||||
PWD := $(shell pwd)
|
||||
|
||||
.PHONY: start stop reload clean
|
||||
|
||||
logs:
|
||||
mkdir -p logs
|
||||
|
||||
start: logs
|
||||
nginx -p $(PWD) -c nginx.conf
|
||||
|
||||
stop:
|
||||
nginx -p $(PWD) -s stop
|
||||
|
||||
reload:
|
||||
nginx -p $(PWD) -s reload
|
||||
|
||||
check:
|
||||
nginx -p $(PWD) -c nginx.conf -t
|
||||
|
||||
clean:
|
||||
rm -rf logs
|
||||
@@ -1,22 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDrTCCApWgAwIBAgIUHqubAnjJikbPDqGiqDT87mbcB+owDQYJKoZIhvcNAQEL
|
||||
BQAwZjELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5
|
||||
MRUwEwYDVQQKDAxPcmdhbml6YXRpb24xDTALBgNVBAsMBFVuaXQxEjAQBgNVBAMM
|
||||
CWxvY2FsaG9zdDAeFw0yNjAxMDgwODAyMzJaFw0yNzAxMDgwODAyMzJaMGYxCzAJ
|
||||
BgNVBAYTAlVTMQ4wDAYDVQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0eTEVMBMGA1UE
|
||||
CgwMT3JnYW5pemF0aW9uMQ0wCwYDVQQLDARVbml0MRIwEAYDVQQDDAlsb2NhbGhv
|
||||
c3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3gozRVkW8EFsOv75Q
|
||||
AgzN0RlOkt+cyesmI71YF0NXOGlYG68Ibb4H7yemX1z663pK3e2MK0k94innW02q
|
||||
0TcBvlu1Emv3Uo+iSTC6hkWR51ZkbfJDjERpO37fZhmdGexHAR2JOLGGTkTkbsRu
|
||||
/lKg8HBHKcEL2NBI1D6LsOU3n+m9p0eSytzE92qY4hRtxaPgK4UUpMaEq64v8NDk
|
||||
HfMFjLXmuxCSUpRTowh4MCcSS1hJCD7SJmsdXHOTQc3dgc/WJOP1rkvCZRUHVaR6
|
||||
ujAIsUo9R2esevrWvxZGXFmJJDuq7u1SNnkArQludD93kovkITjb8+KsdhFGkMJR
|
||||
xzdxAgMBAAGjUzBRMB0GA1UdDgQWBBSaoIBAFJGjfUpC04hJ0AlMVBuXOzAfBgNV
|
||||
HSMEGDAWgBSaoIBAFJGjfUpC04hJ0AlMVBuXOzAPBgNVHRMBAf8EBTADAQH/MA0G
|
||||
CSqGSIb3DQEBCwUAA4IBAQCSuLPsFF4ETql1OLr6p5FqzP6+Cg8VVkzaMJskUBA/
|
||||
MEwZij+wR6teoAv8AzptByHoycOLRjYXDgweL5Z37Vr1Fo8bSqzGPqrGh6YkBNTZ
|
||||
heXFf8g5lOWiv3OypxVtkEFD1tYhdU//5tnipHh08dEjZBuTumMMYhr6xcYBecI7
|
||||
Lw6uo+TRrPrPLgcJU53Sivm8c2fGVexGfcklvtW4qvkQik+r9BfnoJ/f12PBq2J5
|
||||
nKEQFR9EwULnkqREf0Iw/mDPS275brhdDA8JNlB49GGcRGcYu9tOVGDX5rfdIwXB
|
||||
PTaWwrOoY5F4NVPlY7d5E8eLHSt8/zd+56w4g5YaQUaH
|
||||
-----END CERTIFICATE-----
|
||||
@@ -1,11 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Generate a private key
|
||||
openssl genrsa -out key.pem 2048
|
||||
|
||||
# Generate a self-signed certificate
|
||||
openssl req -new -x509 -key key.pem -out cert.pem -days 365 \
|
||||
-subj "/C=US/ST=State/L=City/O=Organization/OU=Unit/CN=localhost"
|
||||
|
||||
echo "Certificate (cert.pem) and Key (key.pem) generated successfully in $(pwd)"
|
||||
@@ -1,28 +0,0 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC3gozRVkW8EFsO
|
||||
v75QAgzN0RlOkt+cyesmI71YF0NXOGlYG68Ibb4H7yemX1z663pK3e2MK0k94inn
|
||||
W02q0TcBvlu1Emv3Uo+iSTC6hkWR51ZkbfJDjERpO37fZhmdGexHAR2JOLGGTkTk
|
||||
bsRu/lKg8HBHKcEL2NBI1D6LsOU3n+m9p0eSytzE92qY4hRtxaPgK4UUpMaEq64v
|
||||
8NDkHfMFjLXmuxCSUpRTowh4MCcSS1hJCD7SJmsdXHOTQc3dgc/WJOP1rkvCZRUH
|
||||
VaR6ujAIsUo9R2esevrWvxZGXFmJJDuq7u1SNnkArQludD93kovkITjb8+KsdhFG
|
||||
kMJRxzdxAgMBAAECggEAUUf932r7EEWeT3YWcZG24iLPtsHPUb8N7KK/5xjGtwdB
|
||||
gckhSfZxyUngogxzPqC2VhrecMtrcYGPVe5XBGYOmzMZo6Y5NB7Zh+QrNTLr9agl
|
||||
1+3q9IhYdNPpakHq7m7g/MI9tt3po7dfRF7bmt4C804MLTkNTJYIC3oXCgCcRnpJ
|
||||
NYLtombx6FNzivUMB9/FGoR3MeVsenCNmbfGyqssKafsoNfBnszg2XT4W9TpynA5
|
||||
ThER0ZjAu8Wi4QJ4rGuwgCqoobe9qw5xUdnerj84O/FDxY38zK92yZzxPG39O61U
|
||||
kSna0WKAhtvOqYvYe5KvIFs8AbFNBn+/mCEhaPmYEQKBgQDvXjy7maM0L8yDYNs5
|
||||
2Mups0DC6E3Xkx5k6O6/TPfBBY949ZBsAT+hDpJA119/UOxGRE/wZaQ+09blyjOD
|
||||
B8Nw0m0OOgpERIHhm+uDXpkVhFJXkBQMB7Jsc2tZKxHNY78UD4K4YcsbRGBNZryf
|
||||
6Fd6uVad0qMKJF825gN3f7d5twKBgQDEQrxBEensG6D5XWoYTqwvNfgc9BqOAWrJ
|
||||
IkX4l99n9yJUHJ+h7wcrzA1FfKVJU5LkvR+YqCxcSyil5BnQFU7s3JqnhIkwfk9R
|
||||
wS2pPXLaJS6oQQcr2yRQgP8xDDMlBNABIkw/JARXsPkdxM+y93G4s+O4GIsrOJHX
|
||||
zZWjoK74FwKBgDsKjoZjZfsIUzSQnmJNVCbefPnWXOTwd83CGql67Z1hprraLy5E
|
||||
vJIYSwDNXtPDfRPz45fTKkkI7XeugTgvoQhiDt40eLXQBA1G7TdxDOdexQSuGBPD
|
||||
jkHr0i2jTvfPQ6FKFh8ul+941RwiXCJBJ9qwfmndO7PTttyOrYo7KWFPAoGBAJHb
|
||||
jxFFAPaKag4pkKnT/j9v+p96Ayo6f2RP5nSkWlJn5slgJHsyq7+u/MYBps1iIS+C
|
||||
JrNLePx2lxUC+vQcgfWi1gVdiJ/Tn5uA2DCJDIx3Sb+d5HiBevGhjQakrHX8p6HQ
|
||||
HZ5nZBrP2bR4CmxVXd0JRFoTU4qhljIuBeH/j0mtAoGAN9Hf3txLTm7PbuNy0xOM
|
||||
Us+nHsVjIzZvZ6H413kzs85QIf22lw0JEoRBnhtZmxATERrvYJ9YtyZH0Tf+b+eU
|
||||
TioM9wDi62tDjgMK+jgoIPid4hgMdHzdjjxJ6kZ9sFjR/3dOKxDwe8jU1YybFqxH
|
||||
yYRlpVwN2c++XrbOPfE+m7E=
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -1,85 +0,0 @@
|
||||
|
||||
types {
|
||||
text/html html htm shtml;
|
||||
text/css css;
|
||||
text/xml xml;
|
||||
image/gif gif;
|
||||
image/jpeg jpeg jpg;
|
||||
application/javascript js;
|
||||
application/atom+xml atom;
|
||||
application/rss+xml rss;
|
||||
|
||||
text/mathml mml;
|
||||
text/plain txt;
|
||||
text/vnd.sun.j2me.app-descriptor jad;
|
||||
text/vnd.wap.wml wml;
|
||||
text/x-component htc;
|
||||
|
||||
image/png png;
|
||||
image/tiff tif tiff;
|
||||
image/vnd.wap.wbmp wbmp;
|
||||
image/x-icon ico;
|
||||
image/x-jng jng;
|
||||
image/x-ms-bmp bmp;
|
||||
image/svg+xml svg svgz;
|
||||
image/webp webp;
|
||||
|
||||
application/font-woff woff;
|
||||
application/java-archive jar war ear;
|
||||
application/json json;
|
||||
application/mac-binhex40 hqx;
|
||||
application/msword doc;
|
||||
application/pdf pdf;
|
||||
application/postscript ps eps ai;
|
||||
application/rtf rtf;
|
||||
application/vnd.apple.mpegurl m3u8;
|
||||
application/vnd.ms-excel xls;
|
||||
application/vnd.ms-fontobject eot;
|
||||
application/vnd.ms-powerpoint ppt;
|
||||
application/vnd.wap.wmlc wmlc;
|
||||
application/vnd.google-earth.kml+xml kml;
|
||||
application/vnd.google-earth.kmz kmz;
|
||||
application/x-7z-compressed 7z;
|
||||
application/x-cocoa cco;
|
||||
application/x-java-archive-diff jardiff;
|
||||
application/x-java-jnlp-file jnlp;
|
||||
application/x-makeself run;
|
||||
application/x-perl pl pm;
|
||||
application/x-pilot prc pdb;
|
||||
application/x-rar-compressed rar;
|
||||
application/x-redhat-package-manager rpm;
|
||||
application/x-sea sea;
|
||||
application/x-shockwave-flash swf;
|
||||
application/x-stuffit sit;
|
||||
application/x-tcl tcl tk;
|
||||
application/x-x509-ca-cert der pem crt;
|
||||
application/x-xpinstall xpi;
|
||||
application/xhtml+xml xhtml;
|
||||
application/xspf+xml xspf;
|
||||
application/zip zip;
|
||||
|
||||
application/octet-stream bin exe dll;
|
||||
application/octet-stream deb;
|
||||
application/octet-stream dmg;
|
||||
application/octet-stream iso img;
|
||||
application/octet-stream msi msp msm;
|
||||
|
||||
audio/midi mid midi kar;
|
||||
audio/mpeg mp3;
|
||||
audio/ogg ogg;
|
||||
audio/x-m4a m4a;
|
||||
audio/x-realaudio ra;
|
||||
|
||||
video/3gpp 3gpp 3gp;
|
||||
video/mp2t ts;
|
||||
video/mp4 mp4;
|
||||
video/mpeg mpeg mpg;
|
||||
video/quicktime mov;
|
||||
video/webm webm;
|
||||
video/x-flv flv;
|
||||
video/x-m4v m4v;
|
||||
video/x-mng mng;
|
||||
video/x-ms-asf asf x-ms-asf;
|
||||
video/x-ms-wmv wmv;
|
||||
video/x-msvideo avi;
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
|
||||
worker_processes 1;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
sendfile on;
|
||||
keepalive_timeout 65;
|
||||
|
||||
# Standard Configuration from Wiki
|
||||
upstream seaweedfs {
|
||||
server 127.0.0.1:8334;
|
||||
keepalive 20;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 8333 ssl;
|
||||
server_name localhost;
|
||||
|
||||
ssl_certificate cert.pem;
|
||||
ssl_certificate_key key.pem;
|
||||
|
||||
ssl_session_cache shared:SSL:1m;
|
||||
ssl_session_timeout 5m;
|
||||
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
ssl_prefer_server_ciphers on;
|
||||
|
||||
ignore_invalid_headers off;
|
||||
client_max_body_size 0;
|
||||
proxy_buffering off;
|
||||
|
||||
location / {
|
||||
proxy_pass http://seaweedfs;
|
||||
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
proxy_connect_timeout 300;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
proxy_request_buffering off;
|
||||
chunked_transfer_encoding off;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,16 +4,17 @@ import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb"
|
||||
"log"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/operation"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/security"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
util_http "github.com/seaweedfs/seaweedfs/weed/util/http"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -4,14 +4,13 @@ import (
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
util_http "github.com/seaweedfs/seaweedfs/weed/util/http"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Downloads an item from an S3 Bucket in the region configured in the shared config
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
util_http "github.com/seaweedfs/seaweedfs/weed/util/http"
|
||||
"io"
|
||||
"log"
|
||||
"math/rand"
|
||||
@@ -13,8 +14,6 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
util_http "github.com/seaweedfs/seaweedfs/weed/util/http"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
util_http "github.com/seaweedfs/seaweedfs/weed/util/http"
|
||||
"io"
|
||||
"log"
|
||||
"math/rand"
|
||||
@@ -14,8 +15,6 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
util_http "github.com/seaweedfs/seaweedfs/weed/util/http"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -3,11 +3,11 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/operation"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/security"
|
||||
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
|
||||
util2 "github.com/seaweedfs/seaweedfs/weed/util"
|
||||
|
||||
+3
-1
@@ -160,4 +160,6 @@ $(WEED_BINARY): $(TEMPL_GO_FILES) $(GO_FILES)
|
||||
# Auto-generate templ files when .templ files change
|
||||
%_templ.go: %.templ
|
||||
@echo "Regenerating $@ from $<"
|
||||
@templ generate
|
||||
@templ generate
|
||||
|
||||
.PHONY: $(TEMPL_GO_FILES)
|
||||
@@ -37,7 +37,6 @@ type ObjectStoreUser struct {
|
||||
AccessKey string `json:"access_key"`
|
||||
SecretKey string `json:"secret_key"`
|
||||
Permissions []string `json:"permissions"`
|
||||
PolicyNames []string `json:"policy_names"`
|
||||
}
|
||||
|
||||
type ObjectStoreUsersData struct {
|
||||
@@ -53,13 +52,11 @@ type CreateUserRequest struct {
|
||||
Email string `json:"email"`
|
||||
Actions []string `json:"actions"`
|
||||
GenerateKey bool `json:"generate_key"`
|
||||
PolicyNames []string `json:"policy_names"`
|
||||
}
|
||||
|
||||
type UpdateUserRequest struct {
|
||||
Email string `json:"email"`
|
||||
Actions []string `json:"actions"`
|
||||
PolicyNames []string `json:"policy_names"`
|
||||
Email string `json:"email"`
|
||||
Actions []string `json:"actions"`
|
||||
}
|
||||
|
||||
type UpdateUserPoliciesRequest struct {
|
||||
@@ -73,11 +70,10 @@ type AccessKeyInfo struct {
|
||||
}
|
||||
|
||||
type UserDetails struct {
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Actions []string `json:"actions"`
|
||||
PolicyNames []string `json:"policy_names"`
|
||||
AccessKeys []AccessKeyInfo `json:"access_keys"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Actions []string `json:"actions"`
|
||||
AccessKeys []AccessKeyInfo `json:"access_keys"`
|
||||
}
|
||||
|
||||
type FilerNode struct {
|
||||
|
||||
+133
-134
@@ -7,7 +7,6 @@ import (
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -23,7 +22,6 @@ import (
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/mq_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/schema_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/security"
|
||||
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
"github.com/seaweedfs/seaweedfs/weed/wdclient"
|
||||
"google.golang.org/grpc"
|
||||
@@ -97,11 +95,6 @@ type AdminServer struct {
|
||||
|
||||
// Worker gRPC server
|
||||
workerGrpcServer *WorkerGrpcServer
|
||||
|
||||
// Collection statistics caching
|
||||
collectionStatsCache map[string]collectionStats
|
||||
lastCollectionStatsUpdate time.Time
|
||||
collectionStatsCacheThreshold time.Duration
|
||||
}
|
||||
|
||||
// Type definitions moved to types.go
|
||||
@@ -125,14 +118,13 @@ func NewAdminServer(masters string, templateFS http.FileSystem, dataDir string)
|
||||
go masterClient.KeepConnectedToMaster(ctx)
|
||||
|
||||
server := &AdminServer{
|
||||
masterClient: masterClient,
|
||||
templateFS: templateFS,
|
||||
dataDir: dataDir,
|
||||
grpcDialOption: grpcDialOption,
|
||||
cacheExpiration: 10 * time.Second,
|
||||
filerCacheExpiration: 30 * time.Second, // Cache filers for 30 seconds
|
||||
configPersistence: NewConfigPersistence(dataDir),
|
||||
collectionStatsCacheThreshold: 30 * time.Second,
|
||||
masterClient: masterClient,
|
||||
templateFS: templateFS,
|
||||
dataDir: dataDir,
|
||||
grpcDialOption: grpcDialOption,
|
||||
cacheExpiration: 10 * time.Second,
|
||||
filerCacheExpiration: 30 * time.Second, // Cache filers for 30 seconds
|
||||
configPersistence: NewConfigPersistence(dataDir),
|
||||
}
|
||||
|
||||
// Initialize topic retention purger
|
||||
@@ -243,32 +235,57 @@ func (s *AdminServer) GetCredentialManager() *credential.CredentialManager {
|
||||
|
||||
// InvalidateCache method moved to cluster_topology.go
|
||||
|
||||
// GetS3BucketsData retrieves all Object Store buckets and aggregates total storage metrics
|
||||
func (s *AdminServer) GetS3BucketsData() (S3BucketsData, error) {
|
||||
buckets, err := s.GetS3Buckets()
|
||||
if err != nil {
|
||||
return S3BucketsData{}, err
|
||||
}
|
||||
|
||||
var totalSize int64
|
||||
for _, bucket := range buckets {
|
||||
totalSize += bucket.PhysicalSize
|
||||
}
|
||||
|
||||
return S3BucketsData{
|
||||
Buckets: buckets,
|
||||
TotalBuckets: len(buckets),
|
||||
TotalSize: totalSize,
|
||||
LastUpdated: time.Now(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetS3Buckets retrieves all Object Store buckets from the filer and collects size/object data from collections
|
||||
func (s *AdminServer) GetS3Buckets() ([]S3Bucket, error) {
|
||||
var buckets []S3Bucket
|
||||
|
||||
// Collect volume information by collection with caching
|
||||
collectionMap, _ := s.getCollectionStats()
|
||||
// Build a map of collection name to collection data
|
||||
collectionMap := make(map[string]struct {
|
||||
Size int64
|
||||
FileCount int64
|
||||
})
|
||||
|
||||
// Collect volume information by collection
|
||||
err := s.WithMasterClient(func(client master_pb.SeaweedClient) error {
|
||||
resp, err := client.VolumeList(context.Background(), &master_pb.VolumeListRequest{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.TopologyInfo != nil {
|
||||
for _, dc := range resp.TopologyInfo.DataCenterInfos {
|
||||
for _, rack := range dc.RackInfos {
|
||||
for _, node := range rack.DataNodeInfos {
|
||||
for _, diskInfo := range node.DiskInfos {
|
||||
for _, volInfo := range diskInfo.VolumeInfos {
|
||||
collection := volInfo.Collection
|
||||
if collection == "" {
|
||||
collection = "default"
|
||||
}
|
||||
|
||||
if _, exists := collectionMap[collection]; !exists {
|
||||
collectionMap[collection] = struct {
|
||||
Size int64
|
||||
FileCount int64
|
||||
}{}
|
||||
}
|
||||
|
||||
data := collectionMap[collection]
|
||||
data.Size += int64(volInfo.Size)
|
||||
data.FileCount += int64(volInfo.FileCount)
|
||||
collectionMap[collection] = data
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get volume information: %w", err)
|
||||
}
|
||||
|
||||
// Get filer configuration (buckets path and filer group)
|
||||
filerConfig, err := s.getFilerConfig()
|
||||
@@ -306,12 +323,10 @@ func (s *AdminServer) GetS3Buckets() ([]S3Bucket, error) {
|
||||
collectionName := getCollectionName(filerConfig.FilerGroup, bucketName)
|
||||
|
||||
// Get size and object count from collection data
|
||||
var physicalSize int64
|
||||
var logicalSize int64
|
||||
var size int64
|
||||
var objectCount int64
|
||||
if collectionData, exists := collectionMap[collectionName]; exists {
|
||||
physicalSize = collectionData.PhysicalSize
|
||||
logicalSize = collectionData.LogicalSize
|
||||
size = collectionData.Size
|
||||
objectCount = collectionData.FileCount
|
||||
}
|
||||
|
||||
@@ -325,7 +340,7 @@ func (s *AdminServer) GetS3Buckets() ([]S3Bucket, error) {
|
||||
}
|
||||
|
||||
// Get versioning, object lock, and owner information from extended attributes
|
||||
versioningStatus := ""
|
||||
versioningEnabled := false
|
||||
objectLockEnabled := false
|
||||
objectLockMode := ""
|
||||
var objectLockDuration int32 = 0
|
||||
@@ -333,7 +348,7 @@ func (s *AdminServer) GetS3Buckets() ([]S3Bucket, error) {
|
||||
|
||||
if resp.Entry.Extended != nil {
|
||||
// Use shared utility to extract versioning information
|
||||
versioningStatus = extractVersioningFromEntry(resp.Entry)
|
||||
versioningEnabled = extractVersioningFromEntry(resp.Entry)
|
||||
|
||||
// Use shared utility to extract Object Lock information
|
||||
objectLockEnabled, objectLockMode, objectLockDuration = extractObjectLockInfoFromEntry(resp.Entry)
|
||||
@@ -347,13 +362,12 @@ func (s *AdminServer) GetS3Buckets() ([]S3Bucket, error) {
|
||||
bucket := S3Bucket{
|
||||
Name: bucketName,
|
||||
CreatedAt: time.Unix(resp.Entry.Attributes.Crtime, 0),
|
||||
LogicalSize: logicalSize,
|
||||
PhysicalSize: physicalSize,
|
||||
Size: size,
|
||||
ObjectCount: objectCount,
|
||||
LastModified: time.Unix(resp.Entry.Attributes.Mtime, 0),
|
||||
Quota: quota,
|
||||
QuotaEnabled: quotaEnabled,
|
||||
VersioningStatus: versioningStatus,
|
||||
VersioningEnabled: versioningEnabled,
|
||||
ObjectLockEnabled: objectLockEnabled,
|
||||
ObjectLockMode: objectLockMode,
|
||||
ObjectLockDuration: objectLockDuration,
|
||||
@@ -374,7 +388,6 @@ func (s *AdminServer) GetS3Buckets() ([]S3Bucket, error) {
|
||||
}
|
||||
|
||||
// GetBucketDetails retrieves detailed information about a specific bucket
|
||||
// Note: This no longer lists objects for performance reasons. Use GetS3Buckets for size/count data.
|
||||
func (s *AdminServer) GetBucketDetails(bucketName string) (*BucketDetails, error) {
|
||||
// Get filer configuration (buckets path)
|
||||
filerConfig, err := s.getFilerConfig()
|
||||
@@ -382,25 +395,16 @@ func (s *AdminServer) GetBucketDetails(bucketName string) (*BucketDetails, error
|
||||
glog.Warningf("Failed to get filer configuration, using defaults: %v", err)
|
||||
}
|
||||
|
||||
bucketPath := fmt.Sprintf("%s/%s", filerConfig.BucketsPath, bucketName)
|
||||
|
||||
details := &BucketDetails{
|
||||
Bucket: S3Bucket{
|
||||
Name: bucketName,
|
||||
},
|
||||
Objects: []S3Object{},
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// Get collection data for size and object count with caching
|
||||
collectionName := getCollectionName(filerConfig.FilerGroup, bucketName)
|
||||
stats, err := s.getCollectionStats()
|
||||
if err != nil {
|
||||
glog.Warningf("Failed to get collection data: %v", err)
|
||||
// Continue without collection data - use zero values
|
||||
} else if data, ok := stats[collectionName]; ok {
|
||||
details.Bucket.LogicalSize = data.LogicalSize
|
||||
details.Bucket.PhysicalSize = data.PhysicalSize
|
||||
details.Bucket.ObjectCount = data.FileCount
|
||||
}
|
||||
|
||||
err = s.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error {
|
||||
// Get bucket info
|
||||
bucketResp, err := client.LookupDirectoryEntry(context.Background(), &filer_pb.LookupDirectoryEntryRequest{
|
||||
@@ -426,7 +430,7 @@ func (s *AdminServer) GetBucketDetails(bucketName string) (*BucketDetails, error
|
||||
details.Bucket.QuotaEnabled = quotaEnabled
|
||||
|
||||
// Get versioning, object lock, and owner information from extended attributes
|
||||
versioningStatus := ""
|
||||
versioningEnabled := false
|
||||
objectLockEnabled := false
|
||||
objectLockMode := ""
|
||||
var objectLockDuration int32 = 0
|
||||
@@ -434,7 +438,7 @@ func (s *AdminServer) GetBucketDetails(bucketName string) (*BucketDetails, error
|
||||
|
||||
if bucketResp.Entry.Extended != nil {
|
||||
// Use shared utility to extract versioning information
|
||||
versioningStatus = extractVersioningFromEntry(bucketResp.Entry)
|
||||
versioningEnabled = extractVersioningFromEntry(bucketResp.Entry)
|
||||
|
||||
// Use shared utility to extract Object Lock information
|
||||
objectLockEnabled, objectLockMode, objectLockDuration = extractObjectLockInfoFromEntry(bucketResp.Entry)
|
||||
@@ -445,13 +449,14 @@ func (s *AdminServer) GetBucketDetails(bucketName string) (*BucketDetails, error
|
||||
}
|
||||
}
|
||||
|
||||
details.Bucket.VersioningStatus = versioningStatus
|
||||
details.Bucket.VersioningEnabled = versioningEnabled
|
||||
details.Bucket.ObjectLockEnabled = objectLockEnabled
|
||||
details.Bucket.ObjectLockMode = objectLockMode
|
||||
details.Bucket.ObjectLockDuration = objectLockDuration
|
||||
details.Bucket.Owner = owner
|
||||
|
||||
return nil
|
||||
// List objects in bucket (recursively)
|
||||
return s.listBucketObjects(client, bucketPath, bucketPath, "", details)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@@ -461,6 +466,67 @@ func (s *AdminServer) GetBucketDetails(bucketName string) (*BucketDetails, error
|
||||
return details, nil
|
||||
}
|
||||
|
||||
// listBucketObjects recursively lists all objects in a bucket
|
||||
// bucketBasePath is the full path to the bucket (e.g., /buckets/mybucket)
|
||||
func (s *AdminServer) listBucketObjects(client filer_pb.SeaweedFilerClient, bucketBasePath, directory, prefix string, details *BucketDetails) error {
|
||||
stream, err := client.ListEntries(context.Background(), &filer_pb.ListEntriesRequest{
|
||||
Directory: directory,
|
||||
Prefix: prefix,
|
||||
StartFromFileName: "",
|
||||
InclusiveStartFrom: false,
|
||||
Limit: 1000,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for {
|
||||
resp, err := stream.Recv()
|
||||
if err != nil {
|
||||
if err.Error() == "EOF" {
|
||||
break
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
entry := resp.Entry
|
||||
if entry.IsDirectory {
|
||||
// Recursively list subdirectories
|
||||
subDir := fmt.Sprintf("%s/%s", directory, entry.Name)
|
||||
err := s.listBucketObjects(client, bucketBasePath, subDir, "", details)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// Add file object
|
||||
objectKey := entry.Name
|
||||
if directory != bucketBasePath {
|
||||
// Remove bucket prefix to get relative path
|
||||
relativePath := directory[len(bucketBasePath)+1:]
|
||||
objectKey = fmt.Sprintf("%s/%s", relativePath, entry.Name)
|
||||
}
|
||||
|
||||
obj := S3Object{
|
||||
Key: objectKey,
|
||||
Size: int64(entry.Attributes.FileSize),
|
||||
LastModified: time.Unix(entry.Attributes.Mtime, 0),
|
||||
ETag: "", // Could be calculated from chunks if needed
|
||||
StorageClass: "STANDARD",
|
||||
}
|
||||
|
||||
details.Objects = append(details.Objects, obj)
|
||||
details.TotalSize += obj.Size
|
||||
details.TotalCount++
|
||||
}
|
||||
}
|
||||
|
||||
// Update bucket totals
|
||||
details.Bucket.Size = details.TotalSize
|
||||
details.Bucket.ObjectCount = details.TotalCount
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateS3Bucket creates a new S3 bucket
|
||||
func (s *AdminServer) CreateS3Bucket(bucketName string) error {
|
||||
return s.CreateS3BucketWithQuota(bucketName, 0, false)
|
||||
@@ -517,7 +583,7 @@ func (s *AdminServer) DeleteS3Bucket(bucketName string) error {
|
||||
}
|
||||
|
||||
// GetObjectStoreUsers retrieves object store users from identity.json
|
||||
func (s *AdminServer) GetObjectStoreUsers(ctx context.Context) ([]ObjectStoreUser, error) {
|
||||
func (s *AdminServer) GetObjectStoreUsers() ([]ObjectStoreUser, error) {
|
||||
s3cfg := &iam_pb.S3ApiConfiguration{}
|
||||
|
||||
// Load IAM configuration from filer
|
||||
@@ -550,11 +616,6 @@ func (s *AdminServer) GetObjectStoreUsers(ctx context.Context) ([]ObjectStoreUse
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip service accounts - they should not be parent users
|
||||
if strings.HasPrefix(identity.Name, serviceAccountPrefix) {
|
||||
continue
|
||||
}
|
||||
|
||||
user := ObjectStoreUser{
|
||||
Username: identity.Name,
|
||||
Permissions: identity.Actions,
|
||||
@@ -1841,8 +1902,9 @@ func extractObjectLockInfoFromEntry(entry *filer_pb.Entry) (bool, string, int32)
|
||||
}
|
||||
|
||||
// Function to extract versioning information from bucket entry using shared utilities
|
||||
func extractVersioningFromEntry(entry *filer_pb.Entry) string {
|
||||
return s3api.GetVersioningStatus(entry)
|
||||
func extractVersioningFromEntry(entry *filer_pb.Entry) bool {
|
||||
enabled, _ := s3api.LoadVersioningFromExtended(entry)
|
||||
return enabled
|
||||
}
|
||||
|
||||
// GetConfigPersistence returns the config persistence manager
|
||||
@@ -2002,66 +2064,3 @@ func getBoolFromMap(m map[string]interface{}, key string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type collectionStats struct {
|
||||
PhysicalSize int64
|
||||
LogicalSize int64
|
||||
FileCount int64
|
||||
}
|
||||
|
||||
func collectCollectionStats(topologyInfo *master_pb.TopologyInfo) map[string]collectionStats {
|
||||
collectionMap := make(map[string]collectionStats)
|
||||
for _, dc := range topologyInfo.DataCenterInfos {
|
||||
for _, rack := range dc.RackInfos {
|
||||
for _, node := range rack.DataNodeInfos {
|
||||
for _, diskInfo := range node.DiskInfos {
|
||||
for _, volInfo := range diskInfo.VolumeInfos {
|
||||
collection := volInfo.Collection
|
||||
if collection == "" {
|
||||
collection = "default"
|
||||
}
|
||||
|
||||
data := collectionMap[collection]
|
||||
data.PhysicalSize += int64(volInfo.Size)
|
||||
rp, _ := super_block.NewReplicaPlacementFromByte(byte(volInfo.ReplicaPlacement))
|
||||
// NewReplicaPlacementFromByte never returns a nil rp. If there's an error,
|
||||
// it returns a zero-valued ReplicaPlacement, for which GetCopyCount() is 1.
|
||||
// This provides a safe fallback, so we can ignore the error.
|
||||
replicaCount := int64(rp.GetCopyCount())
|
||||
if volInfo.Size >= volInfo.DeletedByteCount {
|
||||
data.LogicalSize += int64(volInfo.Size-volInfo.DeletedByteCount) / replicaCount
|
||||
}
|
||||
if volInfo.FileCount >= volInfo.DeleteCount {
|
||||
data.FileCount += int64(volInfo.FileCount-volInfo.DeleteCount) / replicaCount
|
||||
}
|
||||
collectionMap[collection] = data
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return collectionMap
|
||||
}
|
||||
|
||||
// getCollectionStats returns current collection statistics with caching
|
||||
func (s *AdminServer) getCollectionStats() (map[string]collectionStats, error) {
|
||||
now := time.Now()
|
||||
if s.collectionStatsCache != nil && now.Sub(s.lastCollectionStatsUpdate) < s.collectionStatsCacheThreshold {
|
||||
return s.collectionStatsCache, nil
|
||||
}
|
||||
|
||||
err := s.WithMasterClient(func(client master_pb.SeaweedClient) error {
|
||||
resp, err := client.VolumeList(context.Background(), &master_pb.VolumeListRequest{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.TopologyInfo != nil {
|
||||
s.collectionStatsCache = collectCollectionStats(resp.TopologyInfo)
|
||||
s.lastCollectionStatsUpdate = now
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return s.collectionStatsCache, err
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package dash
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
@@ -26,31 +25,17 @@ func (s *AdminServer) ShowLogin(c *gin.Context) {
|
||||
}
|
||||
|
||||
// HandleLogin handles login form submission
|
||||
func (s *AdminServer) HandleLogin(adminUser, adminPassword, readOnlyUser, readOnlyPassword string) gin.HandlerFunc {
|
||||
func (s *AdminServer) HandleLogin(username, password string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
loginUsername := c.PostForm("username")
|
||||
loginPassword := c.PostForm("password")
|
||||
|
||||
var role string
|
||||
var authenticated bool
|
||||
|
||||
// Check admin credentials
|
||||
if adminPassword != "" && loginUsername == adminUser && subtle.ConstantTimeCompare([]byte(loginPassword), []byte(adminPassword)) == 1 {
|
||||
role = "admin"
|
||||
authenticated = true
|
||||
} else if readOnlyPassword != "" && loginUsername == readOnlyUser && subtle.ConstantTimeCompare([]byte(loginPassword), []byte(readOnlyPassword)) == 1 {
|
||||
// Check read-only credentials
|
||||
role = "readonly"
|
||||
authenticated = true
|
||||
}
|
||||
|
||||
if authenticated {
|
||||
if loginUsername == username && loginPassword == password {
|
||||
session := sessions.Default(c)
|
||||
// Clear any existing invalid session data before setting new values
|
||||
session.Clear()
|
||||
session.Set("authenticated", true)
|
||||
session.Set("username", loginUsername)
|
||||
session.Set("role", role)
|
||||
if err := session.Save(); err != nil {
|
||||
// Log the detailed error server-side for diagnostics
|
||||
glog.Errorf("Failed to save session for user %s: %v", loginUsername, err)
|
||||
|
||||
@@ -48,13 +48,26 @@ type CreateBucketRequest struct {
|
||||
func (s *AdminServer) ShowS3Buckets(c *gin.Context) {
|
||||
username := c.GetString("username")
|
||||
|
||||
data, err := s.GetS3BucketsData()
|
||||
buckets, err := s.GetS3Buckets()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get Object Store buckets: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
data.Username = username
|
||||
// Calculate totals
|
||||
var totalSize int64
|
||||
for _, bucket := range buckets {
|
||||
totalSize += bucket.Size
|
||||
}
|
||||
|
||||
data := S3BucketsData{
|
||||
Username: username,
|
||||
Buckets: buckets,
|
||||
TotalBuckets: len(buckets),
|
||||
TotalSize: totalSize,
|
||||
LastUpdated: time.Now(),
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, data)
|
||||
}
|
||||
|
||||
@@ -112,12 +125,6 @@ func (s *AdminServer) CreateBucket(c *gin.Context) {
|
||||
// Convert quota to bytes
|
||||
quotaBytes := convertQuotaToBytes(req.QuotaSize, req.QuotaUnit)
|
||||
|
||||
// Validate quota: if enabled, size must be greater than 0
|
||||
if req.QuotaEnabled && quotaBytes <= 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Quota size must be greater than 0 when quota is enabled"})
|
||||
return
|
||||
}
|
||||
|
||||
// Sanitize owner: trim whitespace and enforce max length
|
||||
owner := strings.TrimSpace(req.Owner)
|
||||
if len(owner) > MaxOwnerNameLength {
|
||||
@@ -459,19 +466,16 @@ func (s *AdminServer) CreateS3BucketWithObjectLock(bucketName string, quotaBytes
|
||||
// Handle Object Lock configuration using shared utilities
|
||||
if objectLockEnabled {
|
||||
var duration int32 = 0
|
||||
var mode string = ""
|
||||
|
||||
if setDefaultRetention {
|
||||
// Validate Object Lock parameters only when setting default retention
|
||||
if err := s3api.ValidateObjectLockParameters(objectLockEnabled, objectLockMode, objectLockDuration); err != nil {
|
||||
return fmt.Errorf("invalid Object Lock parameters: %w", err)
|
||||
}
|
||||
duration = objectLockDuration
|
||||
mode = objectLockMode
|
||||
}
|
||||
|
||||
// Create Object Lock configuration using shared utility
|
||||
objectLockConfig := s3api.CreateObjectLockConfigurationFromParams(objectLockEnabled, mode, duration)
|
||||
objectLockConfig := s3api.CreateObjectLockConfigurationFromParams(objectLockEnabled, objectLockMode, duration)
|
||||
|
||||
// Store Object Lock configuration in extended attributes using shared utility
|
||||
if err := s3api.StoreObjectLockConfigurationInExtended(bucketEntry, objectLockConfig); err != nil {
|
||||
|
||||
@@ -120,10 +120,8 @@ func (s *AdminServer) getTopologyViaGRPC(topology *ClusterTopology) error {
|
||||
|
||||
// InvalidateCache forces a refresh of cached data
|
||||
func (s *AdminServer) InvalidateCache() {
|
||||
s.lastCacheUpdate = time.Now().Add(-s.cacheExpiration)
|
||||
s.lastCacheUpdate = time.Time{}
|
||||
s.cachedTopology = nil
|
||||
s.lastFilerUpdate = time.Now().Add(-s.filerCacheExpiration)
|
||||
s.lastFilerUpdate = time.Time{}
|
||||
s.cachedFilers = nil
|
||||
s.lastCollectionStatsUpdate = time.Now().Add(-s.collectionStatsCacheThreshold)
|
||||
s.collectionStatsCache = nil
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@ package dash
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -33,49 +34,34 @@ type BreadcrumbItem struct {
|
||||
|
||||
// FileBrowserData contains all data needed for the file browser view
|
||||
type FileBrowserData struct {
|
||||
Username string `json:"username"`
|
||||
CurrentPath string `json:"current_path"`
|
||||
ParentPath string `json:"parent_path"`
|
||||
Breadcrumbs []BreadcrumbItem `json:"breadcrumbs"`
|
||||
Entries []FileEntry `json:"entries"`
|
||||
|
||||
LastUpdated time.Time `json:"last_updated"`
|
||||
IsBucketPath bool `json:"is_bucket_path"`
|
||||
BucketName string `json:"bucket_name"`
|
||||
// Pagination fields
|
||||
PageSize int `json:"page_size"`
|
||||
HasNextPage bool `json:"has_next_page"`
|
||||
LastFileName string `json:"last_file_name"` // Cursor for next page
|
||||
CurrentLastFileName string `json:"current_last_file_name"` // Cursor from current request (for page size changes)
|
||||
Username string `json:"username"`
|
||||
CurrentPath string `json:"current_path"`
|
||||
ParentPath string `json:"parent_path"`
|
||||
Breadcrumbs []BreadcrumbItem `json:"breadcrumbs"`
|
||||
Entries []FileEntry `json:"entries"`
|
||||
TotalEntries int `json:"total_entries"`
|
||||
TotalSize int64 `json:"total_size"`
|
||||
LastUpdated time.Time `json:"last_updated"`
|
||||
IsBucketPath bool `json:"is_bucket_path"`
|
||||
BucketName string `json:"bucket_name"`
|
||||
}
|
||||
|
||||
// GetFileBrowser retrieves file browser data for a given path with cursor-based pagination
|
||||
func (s *AdminServer) GetFileBrowser(dir string, lastFileName string, pageSize int) (*FileBrowserData, error) {
|
||||
if dir == "" {
|
||||
dir = "/"
|
||||
}
|
||||
|
||||
// Set defaults for pagination
|
||||
if pageSize < 1 {
|
||||
pageSize = 20 // Default page size
|
||||
// GetFileBrowser retrieves file browser data for a given path
|
||||
func (s *AdminServer) GetFileBrowser(path string) (*FileBrowserData, error) {
|
||||
if path == "" {
|
||||
path = "/"
|
||||
}
|
||||
|
||||
var entries []FileEntry
|
||||
var totalSize int64
|
||||
|
||||
// Fetch entries using cursor-based pagination
|
||||
// We fetch pageSize+1 to determine if there's a next page
|
||||
fetchLimit := pageSize + 1
|
||||
var fetchedCount int
|
||||
var lastEntryName string
|
||||
|
||||
// Get directory listing from filer
|
||||
err := s.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error {
|
||||
// Fetch entries starting from the cursor (lastFileName)
|
||||
stream, err := client.ListEntries(context.Background(), &filer_pb.ListEntriesRequest{
|
||||
Directory: dir,
|
||||
Directory: path,
|
||||
Prefix: "",
|
||||
Limit: uint32(fetchLimit),
|
||||
StartFromFileName: lastFileName,
|
||||
InclusiveStartFrom: false, // Don't include the cursor file itself
|
||||
Limit: 1000,
|
||||
InclusiveStartFrom: false,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -95,102 +81,101 @@ func (s *AdminServer) GetFileBrowser(dir string, lastFileName string, pageSize i
|
||||
continue
|
||||
}
|
||||
|
||||
fetchedCount++
|
||||
fullPath := path
|
||||
if !strings.HasSuffix(fullPath, "/") {
|
||||
fullPath += "/"
|
||||
}
|
||||
fullPath += entry.Name
|
||||
|
||||
// Only add entries up to pageSize (the +1 is just to check for next page)
|
||||
if fetchedCount <= pageSize {
|
||||
fullPath := path.Join(dir, entry.Name)
|
||||
var modTime time.Time
|
||||
if entry.Attributes != nil && entry.Attributes.Mtime > 0 {
|
||||
modTime = time.Unix(entry.Attributes.Mtime, 0)
|
||||
}
|
||||
|
||||
var modTime time.Time
|
||||
if entry.Attributes != nil && entry.Attributes.Mtime > 0 {
|
||||
modTime = time.Unix(entry.Attributes.Mtime, 0)
|
||||
var mode string
|
||||
var uid, gid uint32
|
||||
var size int64
|
||||
var replication, collection string
|
||||
var ttlSec int32
|
||||
|
||||
if entry.Attributes != nil {
|
||||
mode = FormatFileMode(entry.Attributes.FileMode)
|
||||
uid = entry.Attributes.Uid
|
||||
gid = entry.Attributes.Gid
|
||||
size = int64(entry.Attributes.FileSize)
|
||||
ttlSec = entry.Attributes.TtlSec
|
||||
}
|
||||
|
||||
// Get replication and collection from entry extended attributes or chunks
|
||||
if entry.Extended != nil {
|
||||
if repl, ok := entry.Extended["replication"]; ok {
|
||||
replication = string(repl)
|
||||
}
|
||||
|
||||
var mode string
|
||||
var uid, gid uint32
|
||||
var size int64
|
||||
var replication, collection string
|
||||
var ttlSec int32
|
||||
|
||||
if entry.Attributes != nil {
|
||||
mode = FormatFileMode(entry.Attributes.FileMode)
|
||||
uid = entry.Attributes.Uid
|
||||
gid = entry.Attributes.Gid
|
||||
size = int64(entry.Attributes.FileSize)
|
||||
ttlSec = entry.Attributes.TtlSec
|
||||
if coll, ok := entry.Extended["collection"]; ok {
|
||||
collection = string(coll)
|
||||
}
|
||||
}
|
||||
|
||||
// Get replication and collection from entry extended attributes
|
||||
if entry.Extended != nil {
|
||||
if repl, ok := entry.Extended["replication"]; ok {
|
||||
replication = string(repl)
|
||||
}
|
||||
if coll, ok := entry.Extended["collection"]; ok {
|
||||
collection = string(coll)
|
||||
}
|
||||
// Determine MIME type based on file extension
|
||||
mime := "application/octet-stream"
|
||||
if entry.IsDirectory {
|
||||
mime = "inode/directory"
|
||||
} else {
|
||||
ext := strings.ToLower(filepath.Ext(entry.Name))
|
||||
switch ext {
|
||||
case ".txt", ".log":
|
||||
mime = "text/plain"
|
||||
case ".html", ".htm":
|
||||
mime = "text/html"
|
||||
case ".css":
|
||||
mime = "text/css"
|
||||
case ".js":
|
||||
mime = "application/javascript"
|
||||
case ".json":
|
||||
mime = "application/json"
|
||||
case ".xml":
|
||||
mime = "application/xml"
|
||||
case ".pdf":
|
||||
mime = "application/pdf"
|
||||
case ".jpg", ".jpeg":
|
||||
mime = "image/jpeg"
|
||||
case ".png":
|
||||
mime = "image/png"
|
||||
case ".gif":
|
||||
mime = "image/gif"
|
||||
case ".svg":
|
||||
mime = "image/svg+xml"
|
||||
case ".mp4":
|
||||
mime = "video/mp4"
|
||||
case ".mp3":
|
||||
mime = "audio/mpeg"
|
||||
case ".zip":
|
||||
mime = "application/zip"
|
||||
case ".tar":
|
||||
mime = "application/x-tar"
|
||||
case ".gz":
|
||||
mime = "application/gzip"
|
||||
}
|
||||
}
|
||||
|
||||
// Determine MIME type based on file extension
|
||||
mime := "application/octet-stream"
|
||||
if entry.IsDirectory {
|
||||
mime = "inode/directory"
|
||||
} else {
|
||||
ext := strings.ToLower(path.Ext(entry.Name))
|
||||
switch ext {
|
||||
case ".txt", ".log":
|
||||
mime = "text/plain"
|
||||
case ".html", ".htm":
|
||||
mime = "text/html"
|
||||
case ".css":
|
||||
mime = "text/css"
|
||||
case ".js":
|
||||
mime = "application/javascript"
|
||||
case ".json":
|
||||
mime = "application/json"
|
||||
case ".xml":
|
||||
mime = "application/xml"
|
||||
case ".pdf":
|
||||
mime = "application/pdf"
|
||||
case ".jpg", ".jpeg":
|
||||
mime = "image/jpeg"
|
||||
case ".png":
|
||||
mime = "image/png"
|
||||
case ".gif":
|
||||
mime = "image/gif"
|
||||
case ".svg":
|
||||
mime = "image/svg+xml"
|
||||
case ".mp4":
|
||||
mime = "video/mp4"
|
||||
case ".mp3":
|
||||
mime = "audio/mpeg"
|
||||
case ".zip":
|
||||
mime = "application/zip"
|
||||
case ".tar":
|
||||
mime = "application/x-tar"
|
||||
case ".gz":
|
||||
mime = "application/gzip"
|
||||
}
|
||||
}
|
||||
|
||||
fileEntry := FileEntry{
|
||||
Name: entry.Name,
|
||||
FullPath: fullPath,
|
||||
IsDirectory: entry.IsDirectory,
|
||||
Size: size,
|
||||
ModTime: modTime,
|
||||
Mode: mode,
|
||||
Uid: uid,
|
||||
Gid: gid,
|
||||
Mime: mime,
|
||||
Replication: replication,
|
||||
Collection: collection,
|
||||
TtlSec: ttlSec,
|
||||
}
|
||||
|
||||
entries = append(entries, fileEntry)
|
||||
|
||||
lastEntryName = entry.Name
|
||||
fileEntry := FileEntry{
|
||||
Name: entry.Name,
|
||||
FullPath: fullPath,
|
||||
IsDirectory: entry.IsDirectory,
|
||||
Size: size,
|
||||
ModTime: modTime,
|
||||
Mode: mode,
|
||||
Uid: uid,
|
||||
Gid: gid,
|
||||
Mime: mime,
|
||||
Replication: replication,
|
||||
Collection: collection,
|
||||
TtlSec: ttlSec,
|
||||
}
|
||||
|
||||
entries = append(entries, fileEntry)
|
||||
if !entry.IsDirectory {
|
||||
totalSize += size
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,16 +186,21 @@ func (s *AdminServer) GetFileBrowser(dir string, lastFileName string, pageSize i
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Determine if there's a next page
|
||||
hasNextPage := fetchedCount > pageSize
|
||||
// Sort entries: directories first, then files, both alphabetically
|
||||
sort.Slice(entries, func(i, j int) bool {
|
||||
if entries[i].IsDirectory != entries[j].IsDirectory {
|
||||
return entries[i].IsDirectory
|
||||
}
|
||||
return strings.ToLower(entries[i].Name) < strings.ToLower(entries[j].Name)
|
||||
})
|
||||
|
||||
// Generate breadcrumbs
|
||||
breadcrumbs := s.generateBreadcrumbs(dir)
|
||||
breadcrumbs := s.generateBreadcrumbs(path)
|
||||
|
||||
// Calculate parent path
|
||||
parentPath := "/"
|
||||
if dir != "/" {
|
||||
parentPath = path.Dir(dir)
|
||||
if path != "/" {
|
||||
parentPath = filepath.Dir(path)
|
||||
if parentPath == "." {
|
||||
parentPath = "/"
|
||||
}
|
||||
@@ -219,33 +209,29 @@ func (s *AdminServer) GetFileBrowser(dir string, lastFileName string, pageSize i
|
||||
// Check if this is a bucket path
|
||||
isBucketPath := false
|
||||
bucketName := ""
|
||||
if strings.HasPrefix(dir, "/buckets/") {
|
||||
if strings.HasPrefix(path, "/buckets/") {
|
||||
isBucketPath = true
|
||||
pathParts := strings.Split(strings.Trim(dir, "/"), "/")
|
||||
pathParts := strings.Split(strings.Trim(path, "/"), "/")
|
||||
if len(pathParts) >= 2 {
|
||||
bucketName = pathParts[1]
|
||||
}
|
||||
}
|
||||
|
||||
return &FileBrowserData{
|
||||
CurrentPath: dir,
|
||||
ParentPath: parentPath,
|
||||
Breadcrumbs: breadcrumbs,
|
||||
Entries: entries,
|
||||
|
||||
CurrentPath: path,
|
||||
ParentPath: parentPath,
|
||||
Breadcrumbs: breadcrumbs,
|
||||
Entries: entries,
|
||||
TotalEntries: len(entries),
|
||||
TotalSize: totalSize,
|
||||
LastUpdated: time.Now(),
|
||||
IsBucketPath: isBucketPath,
|
||||
BucketName: bucketName,
|
||||
// Pagination metadata
|
||||
PageSize: pageSize,
|
||||
HasNextPage: hasNextPage,
|
||||
LastFileName: lastEntryName, // Store for next page navigation
|
||||
CurrentLastFileName: lastFileName, // Store input cursor for page size changes
|
||||
}, nil
|
||||
}
|
||||
|
||||
// generateBreadcrumbs creates breadcrumb navigation for the current path
|
||||
func (s *AdminServer) generateBreadcrumbs(dir string) []BreadcrumbItem {
|
||||
func (s *AdminServer) generateBreadcrumbs(path string) []BreadcrumbItem {
|
||||
var breadcrumbs []BreadcrumbItem
|
||||
|
||||
// Always start with root
|
||||
@@ -254,12 +240,12 @@ func (s *AdminServer) generateBreadcrumbs(dir string) []BreadcrumbItem {
|
||||
Path: "/",
|
||||
})
|
||||
|
||||
if dir == "/" {
|
||||
if path == "/" {
|
||||
return breadcrumbs
|
||||
}
|
||||
|
||||
// Split path and build breadcrumbs
|
||||
parts := strings.Split(strings.Trim(dir, "/"), "/")
|
||||
parts := strings.Split(strings.Trim(path, "/"), "/")
|
||||
currentPath := ""
|
||||
|
||||
for _, part := range parts {
|
||||
@@ -272,7 +258,7 @@ func (s *AdminServer) generateBreadcrumbs(dir string) []BreadcrumbItem {
|
||||
displayName := part
|
||||
if len(breadcrumbs) == 1 && part == "buckets" {
|
||||
displayName = "Object Store Buckets"
|
||||
} else if len(breadcrumbs) == 2 && strings.HasPrefix(dir, "/buckets/") {
|
||||
} else if len(breadcrumbs) == 2 && strings.HasPrefix(path, "/buckets/") {
|
||||
displayName = "📦 " + part // Add bucket icon to bucket name
|
||||
}
|
||||
|
||||
|
||||
@@ -1,502 +0,0 @@
|
||||
package dash
|
||||
|
||||
import (
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
)
|
||||
|
||||
// TestGenerateBreadcrumbs tests the actual breadcrumb generation function
|
||||
// from the production code with various path scenarios
|
||||
func TestGenerateBreadcrumbs(t *testing.T) {
|
||||
s := &AdminServer{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
expected []BreadcrumbItem
|
||||
}{
|
||||
{
|
||||
name: "root path",
|
||||
path: "/",
|
||||
expected: []BreadcrumbItem{
|
||||
{Name: "Root", Path: "/"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "simple path",
|
||||
path: "/folder",
|
||||
expected: []BreadcrumbItem{
|
||||
{Name: "Root", Path: "/"},
|
||||
{Name: "folder", Path: "/folder"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested path",
|
||||
path: "/folder/subfolder",
|
||||
expected: []BreadcrumbItem{
|
||||
{Name: "Root", Path: "/"},
|
||||
{Name: "folder", Path: "/folder"},
|
||||
{Name: "subfolder", Path: "/folder/subfolder"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "bucket path",
|
||||
path: "/buckets/mybucket",
|
||||
expected: []BreadcrumbItem{
|
||||
{Name: "Root", Path: "/"},
|
||||
{Name: "Object Store Buckets", Path: "/buckets"},
|
||||
{Name: "📦 mybucket", Path: "/buckets/mybucket"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "bucket nested path",
|
||||
path: "/buckets/mybucket/folder",
|
||||
expected: []BreadcrumbItem{
|
||||
{Name: "Root", Path: "/"},
|
||||
{Name: "Object Store Buckets", Path: "/buckets"},
|
||||
{Name: "📦 mybucket", Path: "/buckets/mybucket"},
|
||||
{Name: "folder", Path: "/buckets/mybucket/folder"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "path with trailing slash",
|
||||
path: "/folder/",
|
||||
expected: []BreadcrumbItem{
|
||||
{Name: "Root", Path: "/"},
|
||||
{Name: "folder", Path: "/folder"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Call the actual production function
|
||||
result := s.generateBreadcrumbs(tt.path)
|
||||
|
||||
if len(result) != len(tt.expected) {
|
||||
t.Errorf("expected %d breadcrumbs, got %d", len(tt.expected), len(result))
|
||||
return
|
||||
}
|
||||
|
||||
for i, crumb := range result {
|
||||
if crumb.Name != tt.expected[i].Name {
|
||||
t.Errorf("breadcrumb %d: expected name %q, got %q", i, tt.expected[i].Name, crumb.Name)
|
||||
}
|
||||
if crumb.Path != tt.expected[i].Path {
|
||||
t.Errorf("breadcrumb %d: expected path %q, got %q", i, tt.expected[i].Path, crumb.Path)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPathHandlingWithForwardSlashes verifies that the production code
|
||||
// correctly handles paths with forward slashes (not OS-specific backslashes)
|
||||
func TestPathHandlingWithForwardSlashes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
hasSlash bool
|
||||
}{
|
||||
{
|
||||
name: "root",
|
||||
path: "/",
|
||||
hasSlash: true,
|
||||
},
|
||||
{
|
||||
name: "single level",
|
||||
path: "/test",
|
||||
hasSlash: true,
|
||||
},
|
||||
{
|
||||
name: "multiple levels",
|
||||
path: "/a/b/c",
|
||||
hasSlash: true,
|
||||
},
|
||||
{
|
||||
name: "bucket path",
|
||||
path: "/buckets/mybucket/file",
|
||||
hasSlash: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Verify no backslashes appear in paths (which would be wrong on Windows)
|
||||
if strings.Contains(tt.path, "\\") {
|
||||
t.Errorf("path contains backslash: %q", tt.path)
|
||||
}
|
||||
|
||||
// Verify forward slashes are used
|
||||
if tt.hasSlash && !strings.Contains(tt.path, "/") {
|
||||
t.Errorf("path should contain forward slash: %q", tt.path)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestParentPathCalculationLogic verifies that parent path calculation
|
||||
// uses path.Dir semantics (forward slashes), not filepath.Dir (OS-specific)
|
||||
func TestParentPathCalculationLogic(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
currentDir string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "root path",
|
||||
currentDir: "/",
|
||||
expected: "/",
|
||||
},
|
||||
{
|
||||
name: "single level",
|
||||
currentDir: "/folder",
|
||||
expected: "/",
|
||||
},
|
||||
{
|
||||
name: "two levels",
|
||||
currentDir: "/folder/subfolder",
|
||||
expected: "/folder",
|
||||
},
|
||||
{
|
||||
name: "deep nesting",
|
||||
currentDir: "/a/b/c/d",
|
||||
expected: "/a/b/c",
|
||||
},
|
||||
{
|
||||
name: "bucket root",
|
||||
currentDir: "/buckets",
|
||||
expected: "/",
|
||||
},
|
||||
{
|
||||
name: "bucket directory",
|
||||
currentDir: "/buckets/mybucket",
|
||||
expected: "/buckets",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Verify using path.Dir (the correct approach for URLs)
|
||||
// This demonstrates the expected behavior
|
||||
parentPath := "/"
|
||||
if tt.currentDir != "/" {
|
||||
// path.Dir always uses forward slashes
|
||||
parentPath = path.Dir(tt.currentDir)
|
||||
if parentPath == "." {
|
||||
parentPath = "/"
|
||||
}
|
||||
}
|
||||
|
||||
if parentPath != tt.expected {
|
||||
t.Errorf("expected parent %q, got %q for %q", tt.expected, parentPath, tt.currentDir)
|
||||
}
|
||||
|
||||
// Verify no backslashes in the result
|
||||
if strings.Contains(parentPath, "\\") {
|
||||
t.Errorf("parent path contains backslash: %q", parentPath)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestFileExtensionHandlingLogic verifies that file extensions are correctly
|
||||
// identified using path semantics (always forward slashes)
|
||||
func TestFileExtensionHandlingLogic(t *testing.T) {
|
||||
tests := []struct {
|
||||
filename string
|
||||
expected string
|
||||
}{
|
||||
{"file.txt", ".txt"},
|
||||
{"file.log", ".log"},
|
||||
{"archive.tar.gz", ".gz"},
|
||||
{"image.jpg", ".jpg"},
|
||||
{"document.pdf", ".pdf"},
|
||||
{"data.json", ".json"},
|
||||
{"noextension", ""},
|
||||
{".hidden", ".hidden"},
|
||||
{"file.TXT", ".txt"},
|
||||
{"file.JPG", ".jpg"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.filename, func(t *testing.T) {
|
||||
// Verify using path.Ext + strings.ToLower (the correct approach)
|
||||
ext := strings.ToLower(path.Ext(tt.filename))
|
||||
if ext != tt.expected {
|
||||
t.Errorf("expected extension %q for %q, got %q", tt.expected, tt.filename, ext)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestBucketPathDetectionLogic verifies bucket path detection logic
|
||||
func TestBucketPathDetectionLogic(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
isBucket bool
|
||||
expectedName string
|
||||
}{
|
||||
{
|
||||
name: "root is not a bucket path",
|
||||
path: "/",
|
||||
isBucket: false,
|
||||
expectedName: "",
|
||||
},
|
||||
{
|
||||
name: "buckets root",
|
||||
path: "/buckets/",
|
||||
isBucket: true,
|
||||
expectedName: "",
|
||||
},
|
||||
{
|
||||
name: "single bucket",
|
||||
path: "/buckets/mybucket",
|
||||
isBucket: true,
|
||||
expectedName: "mybucket",
|
||||
},
|
||||
{
|
||||
name: "bucket with nested path",
|
||||
path: "/buckets/mybucket/folder/file",
|
||||
isBucket: true,
|
||||
expectedName: "mybucket",
|
||||
},
|
||||
{
|
||||
name: "non-bucket path",
|
||||
path: "/data/folder",
|
||||
isBucket: false,
|
||||
expectedName: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Verify the bucket path detection logic
|
||||
isBucketPath := false
|
||||
bucketName := ""
|
||||
if strings.HasPrefix(tt.path, "/buckets/") {
|
||||
isBucketPath = true
|
||||
pathParts := strings.Split(strings.Trim(tt.path, "/"), "/")
|
||||
if len(pathParts) >= 2 {
|
||||
bucketName = pathParts[1]
|
||||
}
|
||||
}
|
||||
|
||||
if isBucketPath != tt.isBucket {
|
||||
t.Errorf("expected isBucketPath=%v, got %v for path %q", tt.isBucket, isBucketPath, tt.path)
|
||||
}
|
||||
|
||||
if bucketName != tt.expectedName {
|
||||
t.Errorf("expected bucket name %q, got %q for path %q", tt.expectedName, bucketName, tt.path)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPathJoinHandlesEdgeCases verifies that path.Join handles edge cases
|
||||
// properly for URL path construction (unlike filepath.Join which is OS-specific)
|
||||
func TestPathJoinHandlesEdgeCases(t *testing.T) {
|
||||
tests := []struct {
|
||||
testName string
|
||||
dir string
|
||||
filename string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
testName: "root directory",
|
||||
dir: "/",
|
||||
filename: "file.txt",
|
||||
expected: "/file.txt",
|
||||
},
|
||||
{
|
||||
testName: "simple directory",
|
||||
dir: "/folder",
|
||||
filename: "file.txt",
|
||||
expected: "/folder/file.txt",
|
||||
},
|
||||
{
|
||||
testName: "nested directory",
|
||||
dir: "/a/b/c",
|
||||
filename: "file.txt",
|
||||
expected: "/a/b/c/file.txt",
|
||||
},
|
||||
{
|
||||
testName: "handles trailing slash",
|
||||
dir: "/folder/",
|
||||
filename: "file.txt",
|
||||
expected: "/folder/file.txt",
|
||||
},
|
||||
{
|
||||
testName: "handles empty name",
|
||||
dir: "/folder",
|
||||
filename: "",
|
||||
expected: "/folder",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.testName, func(t *testing.T) {
|
||||
// Verify path.Join behavior for URL paths
|
||||
result := path.Join(tt.dir, tt.filename)
|
||||
if result != tt.expected {
|
||||
t.Errorf("path.Join(%q, %q) = %q, expected %q", tt.dir, tt.filename, result, tt.expected)
|
||||
}
|
||||
|
||||
// Verify no backslashes in the result
|
||||
if strings.Contains(result, "\\") {
|
||||
t.Errorf("result contains backslash: %q", result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestWindowsPathNormalizationBehavior validates that Windows-style paths
|
||||
// are correctly converted to forward slashes for URL compatibility.
|
||||
// This test verifies the actual util.CleanWindowsPath() function used in
|
||||
// the ShowFileBrowser handler.
|
||||
func TestWindowsPathNormalizationBehavior(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
windowsPath string
|
||||
expectedNormPath string
|
||||
}{
|
||||
{
|
||||
name: "backslash separator",
|
||||
windowsPath: "\\folder\\subfolder",
|
||||
expectedNormPath: "/folder/subfolder",
|
||||
},
|
||||
{
|
||||
name: "mixed separators",
|
||||
windowsPath: "/folder\\subfolder/file",
|
||||
expectedNormPath: "/folder/subfolder/file",
|
||||
},
|
||||
{
|
||||
name: "already normalized",
|
||||
windowsPath: "/folder/file",
|
||||
expectedNormPath: "/folder/file",
|
||||
},
|
||||
{
|
||||
name: "simple backslash path",
|
||||
windowsPath: "\\data",
|
||||
expectedNormPath: "/data",
|
||||
},
|
||||
{
|
||||
name: "deep nested path",
|
||||
windowsPath: "\\a\\b\\c\\d",
|
||||
expectedNormPath: "/a/b/c/d",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Test the actual production function
|
||||
normalized := util.CleanWindowsPath(tt.windowsPath)
|
||||
if normalized != tt.expectedNormPath {
|
||||
t.Errorf("CleanWindowsPath(%q): expected %q, got %q",
|
||||
tt.windowsPath, tt.expectedNormPath, normalized)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestBreadcrumbPathFormatting validates that breadcrumb paths always
|
||||
// use forward slashes and maintain proper URL format
|
||||
func TestBreadcrumbPathFormatting(t *testing.T) {
|
||||
s := &AdminServer{}
|
||||
|
||||
testPaths := []string{
|
||||
"/",
|
||||
"/folder",
|
||||
"/folder/subfolder",
|
||||
"/buckets/mybucket",
|
||||
"/buckets/mybucket/data",
|
||||
}
|
||||
|
||||
for _, testPath := range testPaths {
|
||||
t.Run("breadcrumbs_for_"+testPath, func(t *testing.T) {
|
||||
breadcrumbs := s.generateBreadcrumbs(testPath)
|
||||
|
||||
// Verify all breadcrumb paths use forward slashes
|
||||
for i, crumb := range breadcrumbs {
|
||||
if strings.Contains(crumb.Path, "\\") {
|
||||
t.Errorf("breadcrumb %d has backslash in path: %q", i, crumb.Path)
|
||||
}
|
||||
// Verify paths start with / (except when empty)
|
||||
if crumb.Path != "" && !strings.HasPrefix(crumb.Path, "/") {
|
||||
t.Errorf("breadcrumb %d path should start with /: %q", i, crumb.Path)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestDirectoryNavigation validates the complete navigation flow
|
||||
// for various path scenarios
|
||||
func TestDirectoryNavigation(t *testing.T) {
|
||||
s := &AdminServer{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
currentPath string
|
||||
expectedParent string
|
||||
expectedCrumbs int
|
||||
}{
|
||||
{
|
||||
name: "navigate from root",
|
||||
currentPath: "/",
|
||||
expectedParent: "/",
|
||||
expectedCrumbs: 1,
|
||||
},
|
||||
{
|
||||
name: "navigate from single folder",
|
||||
currentPath: "/documents",
|
||||
expectedParent: "/",
|
||||
expectedCrumbs: 2,
|
||||
},
|
||||
{
|
||||
name: "navigate from nested folder",
|
||||
currentPath: "/documents/projects/current",
|
||||
expectedParent: "/documents/projects",
|
||||
expectedCrumbs: 4,
|
||||
},
|
||||
{
|
||||
name: "navigate bucket contents",
|
||||
currentPath: "/buckets/data/files",
|
||||
expectedParent: "/buckets/data",
|
||||
expectedCrumbs: 4,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Verify breadcrumbs are generated correctly
|
||||
breadcrumbs := s.generateBreadcrumbs(tt.currentPath)
|
||||
if len(breadcrumbs) != tt.expectedCrumbs {
|
||||
t.Errorf("expected %d breadcrumbs, got %d", tt.expectedCrumbs, len(breadcrumbs))
|
||||
}
|
||||
|
||||
// Verify parent path calculation
|
||||
expectedParent := "/"
|
||||
if tt.currentPath != "/" {
|
||||
expectedParent = path.Dir(tt.currentPath)
|
||||
if expectedParent == "." {
|
||||
expectedParent = "/"
|
||||
}
|
||||
}
|
||||
if expectedParent != tt.expectedParent {
|
||||
t.Errorf("expected parent %q, got %q", tt.expectedParent, expectedParent)
|
||||
}
|
||||
|
||||
// Verify all paths use forward slashes
|
||||
for i, crumb := range breadcrumbs {
|
||||
if strings.Contains(crumb.Path, "\\") {
|
||||
t.Errorf("breadcrumb %d contains backslash: %q", i, crumb.Path)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2,30 +2,17 @@ package dash
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// setAuthContext sets username and role in context for use in handlers
|
||||
func setAuthContext(c *gin.Context, username, role interface{}) {
|
||||
c.Set("username", username)
|
||||
if role != nil {
|
||||
c.Set("role", role)
|
||||
} else {
|
||||
// Default to admin for backward compatibility
|
||||
c.Set("role", "admin")
|
||||
}
|
||||
}
|
||||
|
||||
// RequireAuth checks if user is authenticated
|
||||
func RequireAuth() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
authenticated := session.Get("authenticated")
|
||||
username := session.Get("username")
|
||||
role := session.Get("role")
|
||||
|
||||
if authenticated != true || username == nil {
|
||||
c.Redirect(http.StatusTemporaryRedirect, "/login")
|
||||
@@ -33,8 +20,8 @@ func RequireAuth() gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
// Set username and role in context for use in handlers
|
||||
setAuthContext(c, username, role)
|
||||
// Set username in context for use in handlers
|
||||
c.Set("username", username)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -46,7 +33,6 @@ func RequireAuthAPI() gin.HandlerFunc {
|
||||
session := sessions.Default(c)
|
||||
authenticated := session.Get("authenticated")
|
||||
username := session.Get("username")
|
||||
role := session.Get("role")
|
||||
|
||||
if authenticated != true || username == nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
@@ -57,37 +43,8 @@ func RequireAuthAPI() gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
// Set username and role in context for use in handlers
|
||||
setAuthContext(c, username, role)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// RequireWriteAccess checks if user has admin role (write access)
|
||||
// Returns JSON error for API endpoints, redirects for HTML endpoints
|
||||
func RequireWriteAccess() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
role, exists := c.Get("role")
|
||||
if !exists {
|
||||
role = "admin" // Default for backward compatibility
|
||||
}
|
||||
|
||||
roleStr, ok := role.(string)
|
||||
if !ok || roleStr != "admin" {
|
||||
// Check if this is an API request (path starts with /api) or HTML request
|
||||
path := c.Request.URL.Path
|
||||
if strings.HasPrefix(path, "/api") {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "Insufficient permissions",
|
||||
"message": "This operation requires admin access. Read-only users can only view data.",
|
||||
})
|
||||
} else {
|
||||
c.Redirect(http.StatusSeeOther, "/admin?error=Insufficient permissions")
|
||||
}
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Set username in context for use in handlers
|
||||
c.Set("username", username)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user