Skip to content

feat(seaweedfs): add SeaweedFS object storage provider - #14160

Draft
chrislusf wants to merge 3 commits into
apache:mainfrom
chrislusf:feat/seaweedfs-object-store-provider
Draft

feat(seaweedfs): add SeaweedFS object storage provider#14160
chrislusf wants to merge 3 commits into
apache:mainfrom
chrislusf:feat/seaweedfs-object-store-provider

Conversation

@chrislusf

Copy link
Copy Markdown

Summary

Adds SeaweedFS as a first-class object storage provider in CloudStack, alongside the existing MinIO, Ceph RGW, and Cloudian HyperStore providers.

SeaweedFS exposes an S3-compatible API and an AWS IAM-compatible API, so this provider uses the AWS S3 and IAM Java SDKs — the same approach as the Cloudian HyperStore provider. No proprietary admin client is needed.

Key features

  • Bucket CRUD, policy, versioning, encryption, ACLs via AmazonS3 SDK (AWS SDK v1, same as Ceph/Cloudian)
  • Per-account IAM user provisioning via AmazonIdentityManagement SDK (same as Cloudian HyperStore)
  • Per-bucket quota via the SeaweedFS S3 ?seaweedfs-quota extension (PUT /{bucket}?seaweedfs-quota), authenticated via SigV4 and authorized via the s3:PutBucketQuota IAM permission. This requires SeaweedFS PR Multiple guest networks deployment issue #11279 (merged).
  • Usage reporting via S3 ListObjectsV2 (MVP; Prometheus or SOSAPI capacity.xml recommended for production scale)

Architecture

The plugin follows the Cloudian HyperStore pattern almost line for line:

  • Same store-details keys: s3Url, iamUrl, accesskey, secretkey
  • iamUrl defaults to s3Url (SeaweedFS registers its IAM API at POST / on the same S3 endpoint)
  • Same IAM-user-with-restricted-policy pattern
  • Same Spring wiring

Quota management

SeaweedFS enforces bucket quota server-side (read-only flag when usage exceeds the limit). The configuration surface is a narrow S3 subresource — PUT /{bucket}?seaweedfs-quota — authenticated via standard S3 SigV4 and authorized via dedicated s3:PutBucketQuota/s3:GetBucketQuota IAM permissions. This avoids exposing the broad SeaweedFS admin API to CloudStack. The plugin signs the request with AWSS3V4Signer and sends it via java.net.http.HttpClient (the AWS S3 SDK doesn't natively support custom subresources).

Comparison with MinIO and Ceph

Concern MinIO Ceph RGW Cloudian SeaweedFS
Bucket CRUD MinioClient AmazonS3 AmazonS3 AmazonS3
User creation MinioAdminClient RgwAdmin AmazonIdentityManagement AmazonIdentityManagement
Per-bucket quota MinioAdminClient RgwAdmin not supported S3 extension (SigV4)
Usage reporting MinioAdminClient RgwAdmin Cloudian admin API S3 ListObjectsV2

Files

New module under plugins/storage/object/seaweedfs/:

  • pom.xml — Maven module
  • SeaweedFSObjectStoreProviderImpl.java — Spring provider registration
  • SeaweedFSObjectStoreLifeCycleImpl.java — Pool add/health-check, URL validation
  • SeaweedFSObjectStoreDriverImpl.java — Bucket + user ops via S3 + IAM SDK
  • SeaweedFSObjectStoreUtil.java — S3 + IAM client builders, constants, SigV4 quota request
  • Spring/module metadata
  • Tests (18 tests, all passing)

SeaweedFS dependency

Requires SeaweedFS with the ?seaweedfs-quota S3 extension (PR seaweedfs/seaweedfs#11279, merged). Without it, quota operations will fail with 404; all other operations (bucket CRUD, user provisioning, usage) work with any recent SeaweedFS release.

Test plan

  • Unit tests pass: mvn -pl plugins/storage/object/seaweedfs test (18 tests, 0 failures)
  • Manual: register a SeaweedFS object store via addObjectStoragePool with s3Url, accesskey, secretkey
  • Manual: create a bucket via CloudStack, verify it appears in SeaweedFS
  • Manual: set a bucket quota via CloudStack, verify it appears in SeaweedFS via weed shell s3.bucket.list
  • Manual: provision a user for a CloudStack account, verify IAM user appears in SeaweedFS
  • Manual: verify usage reporting returns bucket sizes

Add a new object storage provider plugin for SeaweedFS, alongside the
existing MinIO, Ceph RGW, and Cloudian HyperStore providers.

SeaweedFS exposes an S3-compatible API and an AWS IAM-compatible API, so
this provider uses the AWS S3 and IAM Java SDKs — the same approach as
the Cloudian HyperStore provider. No proprietary admin client is needed.

Key features:
- Bucket CRUD, policy, versioning, encryption, ACLs via AmazonS3 SDK
- Per-account IAM user provisioning via AmazonIdentityManagement SDK
- Per-bucket quota via the SeaweedFS S3 ?seaweedfs-quota extension
  (PUT /{bucket}?seaweedfs-quota), authenticated via SigV4 and authorized
  via the s3:PutBucketQuota IAM permission. This requires SeaweedFS PR
  apache#11279.
- Usage reporting via S3 ListObjectsV2 (MVP; Prometheus or SOSAPI
  capacity.xml recommended for production scale)

The service credential (accesskey/secretkey on the object store) is
granted only s3:PutBucketQuota and s3:GetBucketQuota via an IAM policy,
so it cannot delete buckets, manage users, or change cluster topology.

The plugin follows the Cloudian HyperStore pattern almost line for line:
same store-details keys (s3Url, iamUrl, accesskey, secretkey), same
IAM-user-with-restricted-policy pattern, same Spring wiring.
SeaweedFS registers its embedded IAM API at POST / on the same S3
endpoint (UnifiedPostHandler in s3api_server.go), not under /iam. The
AWS IAM SDK uses the Query protocol and POSTs to the endpoint root, so
defaulting iamUrl to <s3Url>/iam would send IAM operations to an
unregistered path. Default to s3Url instead; a separate iamUrl is only
needed for deployments running a standalone weed iam server.

Found by Greptile review on PR apache#11279.
…licy constant

Two issues found by CodeRabbit review on PR apache#11279:

1. S3Signer implements legacy S3 Signature Version 2, not SigV4.
   SeaweedFS expects SigV4. Replace with AWSS3V4Signer which implements
   AWS Signature Version 4. The seaweedfs-quota query parameter is
   included in the signed canonical query string.

2. SERVICE_CREDENTIAL_POLICY was a dead constant (never referenced)
   that claimed the service credential is scoped to only
   s3:PutBucketQuota/s3:GetBucketQuota. This contradicts the actual
   implementation, which uses the service credential (admin) for all
   driver operations: bucket CRUD, IAM user provisioning, and quota.
   Remove the dead constant and document the actual credential model.
Copilot AI lite review requested due to automatic review settings September 12, 2026 05:32
@boring-cyborg

boring-cyborg Bot commented Sep 12, 2026

Copy link
Copy Markdown

Congratulations on your first Pull Request and welcome to the Apache CloudStack community! If you have any issues or are unsure about any anything please check our Contribution Guide (https://github.com/apache/cloudstack/blob/main/CONTRIBUTING.md)
Here are some useful points:

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved packaging, lifecycle, IAM isolation, credential handling, and quota-signing issues remain.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds SeaweedFS as a CloudStack object-storage provider using S3/IAM APIs and SeaweedFS quota support.

Changes:

  • Implements provider lifecycle, bucket, IAM, quota, and usage operations.
  • Adds Maven and Spring module registration.
  • Adds provider and driver tests.
File summaries
File Description
plugins/storage/object/seaweedfs/src/test/java/org/apache/cloudstack/storage/datastore/provider/SeaweedFSObjectStoreProviderImplTest.java Tests provider registration.
plugins/storage/object/seaweedfs/src/test/java/org/apache/cloudstack/storage/datastore/driver/SeaweedFSObjectStoreDriverImplTest.java Tests driver behavior and quota handling.
plugins/storage/object/seaweedfs/src/main/resources/META-INF/cloudstack/storage-object-seaweedfs/spring-storage-object-seaweedfs-context.xml Registers Spring provider wiring.
plugins/storage/object/seaweedfs/src/main/resources/META-INF/cloudstack/storage-object-seaweedfs/module.properties Defines module metadata.
plugins/storage/object/seaweedfs/src/main/java/org/apache/cloudstack/storage/datastore/util/SeaweedFSObjectStoreUtil.java Builds clients and quota requests.
plugins/storage/object/seaweedfs/src/main/java/org/apache/cloudstack/storage/datastore/provider/SeaweedFSObjectStoreProviderImpl.java Registers the provider.
plugins/storage/object/seaweedfs/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/SeaweedFSObjectStoreLifeCycleImpl.java Handles pool lifecycle and validation.
plugins/storage/object/seaweedfs/src/main/java/org/apache/cloudstack/storage/datastore/driver/SeaweedFSObjectStoreDriverImpl.java Implements storage and IAM operations.
plugins/storage/object/seaweedfs/pom.xml Defines module dependencies.
plugins/pom.xml Registers the SeaweedFS module.
Review details

Suppressed comments (4)

plugins/storage/object/seaweedfs/src/main/java/org/apache/cloudstack/storage/datastore/driver/SeaweedFSObjectStoreDriverImpl.java:430

  • On any transient per-bucket S3 failure, this inserts 0. BucketApiServiceImpl treats returned values as authoritative and persists bucket and object-store usage, so a temporary timeout resets usage to zero and under-reports capacity. Propagate the failure for the whole store (or skip the store update) instead of publishing zero.
            } catch (AmazonClientException e) {
                logger.warn("Failed to get usage for bucket {}: {}", bucket.getName(), e.getMessage());
                bucketUsage.put(bucket.getName(), 0L);

plugins/storage/object/seaweedfs/src/main/java/org/apache/cloudstack/storage/datastore/util/SeaweedFSObjectStoreUtil.java:271

  • HttpClient.newHttpClient() and this request have no connect or request timeout. A stalled SeaweedFS endpoint can block the synchronous bucket create/update API worker indefinitely. Use a shared client with configured connect and request timeouts, preferably using the provider's existing timeout configuration.
            java.net.http.HttpClient client = java.net.http.HttpClient.newHttpClient();
            java.net.http.HttpResponse<String> response = client.send(reqBuilder.build(),
                    java.net.http.HttpResponse.BodyHandlers.ofString());

plugins/storage/object/seaweedfs/src/main/java/org/apache/cloudstack/storage/datastore/util/SeaweedFSObjectStoreUtil.java:261

  • The request adds Content-Length to the SDK headers at line 243, then copies every header into java.net.http.HttpRequest.Builder. Java's HttpClient rejects Content-Length as a restricted header, so this loop can throw before the request is sent. Filter transport-managed headers (at least Content-Length, and Host if present) and let HttpClient generate them while signing the same body.
            for (java.util.Map.Entry<String, String> entry : request.getHeaders().entrySet()) {
                if (entry.getKey() != null && entry.getValue() != null) {
                    reqBuilder.header(entry.getKey(), entry.getValue());
                }

plugins/storage/object/seaweedfs/src/test/java/org/apache/cloudstack/storage/datastore/driver/SeaweedFSObjectStoreDriverImplTest.java:280

  • The common setUp already populates storeDetailsMap with the S3 URL and both credentials, so this test never exercises the missing-configuration guard; it passes only because the real HTTP call fails. Clear or override the details map and assert the validation message before any network call.
        // No S3 URL/credentials configured — should throw with a clear message
        assertThrows(CloudRuntimeException.class, () -> driver.setBucketQuota(bucketTO, TEST_STORE_ID, 10));
  • Files reviewed: 10/10 changed files
  • Comments generated: 6
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread plugins/pom.xml
<module>storage/object/minio</module>
<module>storage/object/ceph</module>
<module>storage/object/cloudian</module>
<module>storage/object/seaweedfs</module>
Map<String, Object> objectStoreParameters = new HashMap<String, Object>();
objectStoreParameters.put(SeaweedFSObjectStoreUtil.STORE_KEY_NAME, name);
objectStoreParameters.put(SeaweedFSObjectStoreUtil.STORE_KEY_URL, url);
objectStoreParameters.put(SeaweedFSObjectStoreUtil.STORE_KEY_PROVIDER_NAME, providerName);
Comment on lines +70 to +73
" \"Action\": [\n" +
" \"s3:*\"\n" +
" ],\n" +
" \"Resource\": \"*\"\n" +
com.amazonaws.DefaultRequest<?> request = new com.amazonaws.DefaultRequest<>("s3");
request.setEndpoint(endpointUri);
request.setHttpMethod(com.amazonaws.http.HttpMethodName.valueOf(method));
request.setResourcePath(resourcePath);
Comment on lines +139 to +142
// Create a new access key for this user
CreateAccessKeyResult result = iamClient.createAccessKey(
new CreateAccessKeyRequest().withUserName(userName));
AccessKey key = result.getAccessKey();
Comment on lines +256 to +261
// Should not throw for 0 — uses static method, can't easily mock, but
// the test validates the code path doesn't throw before the HTTP call
// Since we can't mock the static HTTP call, we expect a CloudRuntimeException
// from the HTTP call failing (no real server). That's acceptable — it proves
// the code path reaches the S3 extension rather than throwing "not supported".
assertThrows(CloudRuntimeException.class, () -> driver.setBucketQuota(bucketTO, TEST_STORE_ID, 0));
@chrislusf
chrislusf marked this pull request as draft September 12, 2026 05:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants