radosgw-admin
v0.2.0
Published
Node.js SDK for the Ceph RADOS Gateway Admin Ops API — manage users, buckets, quotas and rate limits programmatically
Maintainers
Readme
radosgw-admin
Node.js SDK for the Ceph RADOS Gateway Admin Ops API — manage users, buckets, quotas, rate limits and access keys programmatically.
Why?
The only existing npm package for RGW Admin Ops (rgw-admin-client) was last published 7 years ago — no TypeScript, no ESM, no maintenance. Meanwhile, Ceph adoption in Kubernetes (Rook-Ceph, OpenShift Data Foundation) keeps growing. This package fills that gap.
What you get:
- Full RGW Admin Ops API coverage — users, keys, subusers, buckets, quotas, rate limits
- Zero runtime dependencies — AWS SigV4 signing uses only
node:crypto - Request hooks — add logging, Prometheus metrics, or audit trails via
onBeforeRequest/onAfterResponse - Health check —
rgw.healthCheck()for one-liner connectivity verification - Structured error hierarchy — catch specific failures, not generic HTTP errors
- Automatic snake_case/camelCase conversion — idiomatic JS API over RGW's REST interface
- TypeScript with strict types and zero
any— every request and response is fully typed - Dual ESM + CJS build — works in every Node.js environment
Install
npm install radosgw-admin
# or
yarn add radosgw-admin
# or
pnpm add radosgw-admin
# or
bun add radosgw-adminRequires Node.js >= 18 and a Ceph RGW instance with the Admin Ops API enabled.
Quick Start
import { RadosGWAdminClient } from 'radosgw-admin';
const rgw = new RadosGWAdminClient({
host: 'http://ceph-rgw.example.com',
port: 8080,
accessKey: 'ADMIN_ACCESS_KEY',
secretKey: 'ADMIN_SECRET_KEY',
});
// Create a user
const user = await rgw.users.create({
uid: 'alice',
displayName: 'Alice',
email: '[email protected]',
maxBuckets: 100,
});
// List all users
const uids = await rgw.users.list();
// Get user info with keys, caps, quotas
const info = await rgw.users.get('alice');
// Suspend / re-enable
await rgw.users.suspend('alice');
await rgw.users.enable('alice');
// Delete (with optional purge of all objects)
await rgw.users.delete({ uid: 'alice', purgeData: true });Configuration
const rgw = new RadosGWAdminClient({
host: 'https://ceph-rgw.example.com', // Required — RGW endpoint
port: 443, // Optional — omit to use host URL default
accessKey: 'ADMIN_KEY', // Required — admin user access key
secretKey: 'ADMIN_SECRET', // Required — admin user secret key
adminPath: '/admin', // Optional — API prefix (default: "/admin")
timeout: 15000, // Optional — request timeout in ms (default: 10000)
region: 'us-east-1', // Optional — SigV4 region (default: "us-east-1")
insecure: false, // Optional — skip TLS verification (default: false)
debug: false, // Optional — enable request/response logging (default: false)
maxRetries: 3, // Optional — retry transient errors (default: 0)
retryDelay: 200, // Optional — base delay for exponential backoff w/ jitter in ms (default: 200)
userAgent: 'my-app/1.0', // Optional — custom User-Agent (default: "radosgw-admin/<ver> node/<ver>")
onBeforeRequest: [(ctx) => {}], // Optional — hooks called before each request
onAfterResponse: [(ctx) => {}], // Optional — hooks called after each response
});API Reference
Users
rgw.users.create(input); // Create a new RGW user
rgw.users.get(uid, tenant?); // Get full user info (keys, caps, quotas)
rgw.users.getByAccessKey(accessKey); // Look up a user by their S3 access key
rgw.users.modify(input); // Update display name, email, max buckets, etc.
rgw.users.delete(input); // Delete user (optionally purge all data)
rgw.users.list(); // List all user IDs in the cluster
rgw.users.suspend(uid); // Suspend a user account
rgw.users.enable(uid); // Re-enable a suspended user
rgw.users.getStats(uid, sync?); // Get storage usage statisticsKeys
rgw.keys.generate(input); // Generate or assign S3/Swift keys
rgw.keys.revoke(input); // Revoke (delete) a key pairSubusers.
rgw.subusers.create(input); // Create a subuser for an existing user
rgw.subusers.modify(input); // Modify subuser permissions
rgw.subusers.remove(input); // Remove a subuser// Create a Swift subuser with full access
await rgw.subusers.create({
uid: 'alice',
subuser: 'alice:swift',
access: 'full',
keyType: 'swift',
generateSecret: true,
});
// Restrict to read-only
await rgw.subusers.modify({
uid: 'alice',
subuser: 'alice:swift',
access: 'read',
});
// Remove the subuser
await rgw.subusers.remove({ uid: 'alice', subuser: 'alice:swift' });Buckets
rgw.buckets.list(); // List all buckets in the cluster
rgw.buckets.listByUser(uid); // List buckets owned by a specific user
rgw.buckets.getInfo(bucket); // Get bucket metadata and stats
rgw.buckets.delete(input); // Delete a bucket (optionally purge objects)
rgw.buckets.transferOwnership(input); // Transfer bucket to a different user
rgw.buckets.removeOwnership(input); // Remove bucket ownership
rgw.buckets.verifyIndex(input); // Check and optionally repair bucket index// List all buckets in the cluster
const allBuckets = await rgw.buckets.list();
// List buckets owned by a specific user
const userBuckets = await rgw.buckets.listByUser('alice');
// Get detailed bucket info
const info = await rgw.buckets.getInfo('my-bucket');
console.log(info.usage.rgwMain.numObjects);
// Transfer a bucket to a different user
await rgw.buckets.transferOwnership({
bucket: 'my-bucket',
bucketId: info.id,
uid: 'bob',
});
// Check and repair bucket index
const result = await rgw.buckets.verifyIndex({
bucket: 'my-bucket',
checkObjects: true,
fix: true,
});
// Delete bucket and all its objects
await rgw.buckets.delete({ bucket: 'my-bucket', purgeObjects: true });Quotas
rgw.quota.getUserQuota(uid); // Get user-level quota
rgw.quota.setUserQuota(input); // Set user-level quota (accepts "10G" size strings)
rgw.quota.enableUserQuota(uid); // Enable user quota without changing values
rgw.quota.disableUserQuota(uid); // Disable user quota without changing values
rgw.quota.getBucketQuota(uid); // Get bucket-level quota for a user
rgw.quota.setBucketQuota(input); // Set bucket-level quota
rgw.quota.enableBucketQuota(uid); // Enable bucket quota
rgw.quota.disableBucketQuota(uid); // Disable bucket quotamaxSize accepts a number (bytes) or a human-readable string with binary (1024-based) units:
| Input | Bytes |
|-------|-------|
| '1K' / '1KB' | 1,024 |
| '500M' / '500MB' | 524,288,000 |
| '10G' / '10GB' | 10,737,418,240 |
| '1T' / '1TB' | 1,099,511,627,776 |
| '1.5G' | 1,610,612,736 |
| 1073741824 | 1,073,741,824 (raw bytes) |
| -1 | Unlimited |
// Set a 10GB user quota with 50k object limit
await rgw.quota.setUserQuota({
uid: 'alice',
maxSize: '10G', // → 10737418240 bytes
maxObjects: 50000,
enabled: true, // default: true when setting
});
// Size limit only, unlimited objects
await rgw.quota.setUserQuota({
uid: 'alice',
maxSize: '10G',
maxObjects: -1, // -1 = unlimited
});
// Check current quota — maxSize is always returned as bytes
const quota = await rgw.quota.getUserQuota('alice');
console.log('Enabled:', quota.enabled);
console.log('Max size:', quota.maxSize, 'bytes');
// Disable quota temporarily (preserves values)
await rgw.quota.disableUserQuota('alice');
// Set a 1GB per-bucket quota (applies to all buckets owned by the user)
await rgw.quota.setBucketQuota({
uid: 'alice', // uid, not bucket name — RGW bucket quotas are per-user
maxSize: '1G',
maxObjects: 10000,
});Rate Limits
Requires Ceph Pacific (v16) or later. Values are per RGW instance — divide by the number of RGW daemons for cluster-wide limits.
rgw.rateLimit.getUserLimit(uid); // Get user rate limit
rgw.rateLimit.setUserLimit(input); // Set user rate limit
rgw.rateLimit.disableUserLimit(uid); // Disable user rate limit
rgw.rateLimit.getBucketLimit(bucket); // Get bucket rate limit
rgw.rateLimit.setBucketLimit(input); // Set bucket rate limit
rgw.rateLimit.disableBucketLimit(bucket); // Disable bucket rate limit
rgw.rateLimit.getGlobal(); // Get global rate limits (user/bucket/anonymous)
rgw.rateLimit.setGlobal(input); // Set global rate limit for a scope// Throttle alice to 100 read ops/min and 50MB/min write per RGW instance
await rgw.rateLimit.setUserLimit({
uid: 'alice',
maxReadOps: 100,
maxWriteOps: 50,
maxWriteBytes: 52428800, // 50MB
});
// Disable rate limit temporarily (preserves config)
await rgw.rateLimit.disableUserLimit('alice');
// Set a bucket-level rate limit
await rgw.rateLimit.setBucketLimit({
bucket: 'public-assets',
maxReadOps: 200,
maxWriteOps: 10,
});
// Protect public-read buckets from anonymous abuse
await rgw.rateLimit.setGlobal({
scope: 'anonymous',
maxReadOps: 50,
maxWriteOps: 0,
enabled: true,
});
// View all global rate limits
const global = await rgw.rateLimit.getGlobal();
console.log('Anonymous:', global.anonymous);
console.log('User:', global.user);
console.log('Bucket:', global.bucket);Usage & Analytics
Prerequisite: Usage logging must be enabled in
ceph.conf:rgw enable usage log = true. Restart RGW daemons after changing the config.
rgw.usage.get(input?); // Query usage report (per-user or cluster-wide)
rgw.usage.trim(input?); // Delete old usage logs — requires removeAll: true when no uid// Usage for alice in January 2025
const report = await rgw.usage.get({
uid: 'alice',
start: '2025-01-01', // accepts 'YYYY-MM-DD' or Date object
end: '2025-01-31',
});
for (const summary of report.summary) {
for (const cat of summary.categories) {
console.log(`[${cat.category}] ops: ${cat.ops} | sent: ${(cat.bytesSent / 1e6).toFixed(2)} MB`);
}
console.log('Total sent:', (summary.total.bytesSent / 1e9).toFixed(3), 'GB');
}
// Cluster-wide usage, all time (omit all filters)
const all = await rgw.usage.get();
// Trim a single user's logs up to end of 2024
await rgw.usage.trim({ uid: 'alice', end: '2024-12-31' });
// ⚠️ Trim all cluster usage logs — removeAll: true required when no uid
await rgw.usage.trim({ end: '2023-12-31', removeAll: true });Cluster Info
rgw.info.get(); // Get cluster FSID and basic endpoint infoconst info = await rgw.info.get();
console.log('Cluster FSID:', info.info.storageBackends[0].clusterId);Error Handling
Every error thrown is an instance of RGWError with structured properties:
import { RGWNotFoundError, RGWConflictError, RGWAuthError } from 'radosgw-admin';
try {
await rgw.users.get('nonexistent');
} catch (error) {
if (error instanceof RGWNotFoundError) {
// 404 — user does not exist
} else if (error instanceof RGWAuthError) {
// 403 — check admin credentials / caps
}
}| Error Class | HTTP Status | Retryable | Condition |
| -------------------- | --------------- | --------- | ---------------------------------------- |
| RGWValidationError | 400 / (pre-request) | No | Invalid input (missing uid, bad params) |
| RGWNotFoundError | 404 | No | Resource does not exist |
| RGWConflictError | 409 | No | Resource already exists |
| RGWAuthError | 403 | No | Insufficient credentials or capabilities |
| RGWRateLimitError | 429 | Yes | Rate limit exceeded |
| RGWServiceError | 5xx | Yes | RGW server error |
All errors include a code field with the RGW error code (e.g., NoSuchUser, BucketAlreadyExists, SlowDown).
Note: Destructive operations (
purgeData,purgeObjects) emit aconsole.warnbefore executing. To suppress in CI/automation, redirect stderr or patchconsole.warn.
Health Check
Verify RGW connectivity before running operations:
const ok = await rgw.healthCheck();
if (!ok) throw new Error('Cannot reach RGW');Request Hooks
Add logging, metrics, or telemetry without modifying the SDK:
const rgw = new RadosGWAdminClient({
host: 'http://rgw:8080',
accessKey: '...',
secretKey: '...',
onBeforeRequest: [
(ctx) => console.log(`→ ${ctx.method} ${ctx.path}`),
],
onAfterResponse: [
(ctx) => console.log(`← ${ctx.status} in ${ctx.durationMs}ms`),
],
});Hooks run on every request across all modules. If a hook throws, the error is swallowed — hooks never break RGW operations.
Hook context includes: method, path, url, query, attempt (retry number), startTime, and for after-hooks: status, durationMs, error.
Security note: Hook context includes the full request URL which may contain sensitive query parameters (e.g.
secret-keywhen creating users with pre-specified credentials). If you log or send hook data to external systems, redact sensitive fields. The SDK already redactssecret-keyfrom its own debug logs, but hooks receive the raw URL.
Request Cancellation
Pass an AbortSignal to cancel in-flight requests:
const controller = new AbortController();
setTimeout(() => controller.abort(), 5000);
await rgw._client.request({
method: 'GET',
path: '/user',
signal: controller.signal,
});Compatibility
Tested against Ceph Quincy (v17) and Reef (v18). The Admin Ops API is available in all Ceph releases with RGW.
Prerequisites:
- The RGW admin user must have
users=*,buckets=*capabilities - Admin Ops API must be accessible (default path:
/admin) - For
insecure: true— only use with self-signed certificates in dev/test environments
FAQ
Set insecure: true in the client config. This skips TLS certificate verification — use only in dev/test environments:
const rgw = new RadosGWAdminClient({
host: 'https://rgw.internal',
accessKey: '...',
secretKey: '...',
insecure: true, // skips TLS verification
});The Admin Ops API has been available since Ceph Luminous (v12). This package is tested against Quincy (v17) and Reef (v18).
| Feature | Minimum Ceph Version | |---|---| | Users, keys, subusers, buckets | Luminous (v12) | | Quotas | Luminous (v12) | | Rate limits | Pacific (v16) | | Usage logging | Luminous (v12) |
Enable debug mode to see full HTTP request/response details:
const rgw = new RadosGWAdminClient({
host: 'http://rgw.example.com',
accessKey: '...',
secretKey: '...',
debug: true, // logs request method, URL, headers, and response
});Common issues:
- 403 AccessDenied — admin user lacks required capabilities. Grant with:
radosgw-admin caps add --uid=admin --caps="users=*;buckets=*" - Connection refused — check host/port and that the RGW daemon is running
- Timeout — increase the
timeoutvalue (default: 10000ms) or check network connectivity
Yes. Port-forward or expose the RGW service, then point the client at it:
kubectl port-forward svc/rook-ceph-rgw-my-store 8080:80 -n rook-cephconst rgw = new RadosGWAdminClient({
host: 'http://localhost',
port: 8080,
accessKey: '...',
secretKey: '...',
});To get admin credentials from Rook:
kubectl get secret rook-ceph-dashboard-admin-gateway -n rook-ceph -o jsonpath='{.data.accessKey}' | base64 -d
kubectl get secret rook-ceph-dashboard-admin-gateway -n rook-ceph -o jsonpath='{.data.secretKey}' | base64 -dYes. ODF uses Ceph under the hood. Point the client at the RGW route or service endpoint. The admin credentials are stored in the ocs-storagecluster-ceph-rgw-admin-ops-user secret in the openshift-storage namespace.
AWS SigV4 signing is implemented using only node:crypto (built-in). No aws-sdk, no axios, no node-fetch. This means:
- Smaller
node_modulesfootprint - No supply chain risk from transitive dependencies
- No version conflicts with other packages in your project
Development
git clone https://github.com/nycanshu/radosgw-admin.git
cd radosgw-admin
npm install
npm run build # ESM + CJS via tsup
npm run typecheck # tsc --noEmit (strict)
npm test # vitest
npm run lint # eslint
npm run format # prettier
npm run check # all of the aboveSee CONTRIBUTING.md for guidelines.
License
Apache 2.0 © nycanshu
