An opinionated OpenSharing server for NetApp StorageGRID and Versity S3 Gateway (primary focus is sharing NetApp E-Series data) object stores.
This page has some background on the motivation and use cases.
The OpenSharing Server is Go-based and distributed as a free binary release (Linux on AMD64 and ARM64, Windows) with a Docker Compose stack for evaluation, testing and development (although nothing prevents its use in production, you probably shouldn't).
Tested object stores:
- Versity S3 Gateway 1.8.0 (also with post-release commits (up to at least Sep 9, 2026) required for STS AssumeRoleWithWebIdentity/Iceberg, via its standalone IAM/OIDC service), 1.7.0 (with S3/RDMA as of 8bd73a4 from early August 2026) and 1.6.0
- StorageGRID 12.1, 12.0 (also required for STS AssumeRole/Iceberg)
- Bearer token authorization
- Supports named Delta Tables and Volumes (files)
- Provides metadata discovery-based sharing for Iceberg Tables
- List objects with include/exclude pattern for objects in OpenSharing Volumes
- List sharing candidates limited to Volume candidates, i.e. buckets
- Live reload of sharing configuration
- Vended STS credentials for Iceberg tables, via two interchangeable providers (see Dynamic (only for Iceberg Tables/STS-vended credentials) below):
- StorageGRID 12.0/12.1: classic AssumeRole
- Versity S3 Gateway 1.8.0+: AssumeRoleWithWebIdentity, backed by VGW's own standalone IAM/OIDC service
- Supports S3/RDMA (only with Versity S3 Gateway with S3/RDMA)
Data paths:
- OpenSharing API metadata and policy checks are handled by OpenSharing Go service
- S3 bucket/object discovery and table/volume listings are live S3-compatible API calls
- Iceberg tables: OpenSharing server discovers the "current"
metadata.jsonitself by reading raw S3 listing (version-hint and object sort) - it has no real catalog, and it does not act as Iceberg catalog client. That is convenient for read-only clients and static data where no Iceberg catalog service is available or reachable, such as edge sites, static table repositories, etc. If you have an Iceberg catalog, use it directly.
- Iceberg tables: OpenSharing server discovers the "current"
- S3 object reads are direct
- Presigned URLs are built by OpenSharing server credentials from the configured external S3 API endpoint
- Iceberg tables only: temporary vended credentials, from one of two STS providers - StorageGRID's classic AssumeRole (a separate, static trusted account requests credentials on the caller's behalf) or Versity S3 Gateway's AssumeRoleWithWebIdentity (the caller's own OIDC bearer token is exchanged directly for credentials, no separate trusted account needed)
- When S3/RDMA-enabled Versity S3 Gateway is serving data over an RoCEv2-enabled network, S3/RDMA-capable clients can GET objects over RDMA
Other OpenSharing objects aren't implemented yet because I haven't needed them. Versity S3 Gateway does not support classic AWS STS AssumeRole, but as of 1.8.0 it supports AssumeRoleWithWebIdentity through its own standalone IAM/OIDC service, which OpenSharing also supports for Iceberg tables (see Dynamic (only for Iceberg Tables/STS-vended credentials) below).
- NetApp E-Series with Versity S3 Gateway: securely share data stored on E-Series volumes without copying it to NAS or external S3 or managing complex services
- NetApp StorageGRID: OpenSharing access to Tables, Volumes (buckets) for zero-copy analytics, machine learning, and general file sharing
Currently neither StorageGRID nor E-Series have an official solution for OpenSharing. This server is the first to offer a working implementation and it enables early testing and development using standard, open APIs.
Running from CLI is recommended for quick evaluation, especially with existing S3 storage. It requires access to existing Versity S3 Gateway or StorageGRID S3 API endpoint and valid S3 credentials.
Docker Compose is recommended for evaluation with built-in or own OAuth2.
NOTE: to make it easy to get started, various service files have prepopulated credentials:
./dex-config/config.yaml: Oauth2 credentials and settings such as token TTL./config/shares.yaml: user IDs and bucket configuration./docker-compose.yaml: Versity S3 Gateway admin credentials (s3), and various other inopensharing-app,api
If you are concerned about that, either:
- change the credentials, or
- set up own S3 server instance and run just OpenSharing Server from the CLI using a localhost
HTTP_ADDRandAUTH_REQUIRED=false(see CLI with own S3 service further below)
- Docker Compose comes with a Versity S3 Gateway container (HTTP/27070)
- If you want to use your own S3 service, configure OpenSharing service to use it by editing
docker-compose.yaml- StorageGRID S3 API for tenants usually defaults to HTTPS served on port 10443
- Versity S3 Gateway defaults to HTTP on port 7070 (in
docker-compose.yamlit uses different ports to avoid potential conflict with local instances) - Connecting to HTTPS S3 API targets requires a valid TLS certificate or saved PEM chain (you may put it in
./certs/and set in OpenSharing Server environment variables). You may also disable TLS certificate verification
Built-in Versity S3 Gateway has a Web UI at http://127.0.0.1:27080/. If you want to upload objects from the Web UI, use that. Feel free to customize or hide behind a reverse HTTPS proxy.
There's a number of places where one can screw up, so at least initially, it's recommended to use it as-is or disable the Web UI if you don't want to make it accessible. These are the details you need to get right when accessing the Web UI.
If you want to make the Web UI or S3 API inaccessible to others, change docker-compose.yaml to limit access to localhost clients.
This approach is simple: authentication is disabled, so you can immediately focus on using OpenSharing server.
There are two variants: "static" (presigned URLs, both StorageGRID and Versity S3 Gateway) and "dynamic" (vended STS credentials for Iceberg tables, via either StorageGRID AssumeRole or Versity S3 Gateway AssumeRoleWithWebIdentity - see further below).
Edit ./config/shares.yaml (including storageLocation which has the S3 bucket detail) if you want to use own data, or create the same bucket and upload the sample data from ./sample-data/ to avoid editing shares.yaml. Remove Iceberg shares as they won't work in this mode anyway.
$ export AUTH_REQUIRED=false \
HTTP_ADDR=127.0.0.1:8000 \
S3_INTERNAL_ENDPOINT=https://192.168.1.211:10443 \
S3_EXTERNAL_ENDPOINT=https://192.168.1.211:10443 \
S3_ACCESS_KEY=bla \
S3_SECRET_KEY=bla-bla \
REGISTRY_PATH=config/shares.yaml \
S3_CA_BUNDLE=$PWD/certs/storagegrid-ca.crt \
S3_TLS_VERIFY=true
$ ./opensharing-serverHTTP_ADDR controls bind address and port. The default is :8000 (all interfaces). For local-only CLI testing, prefer 127.0.0.1:8000.
NOTE: For "experimental" S3/RDMA with VGW RDMA, see the section at the bottom. There's no standard S3/RDMA client, so this isn't included in "standard" evaluation.
Windows version (PowerShell 7 terminal):
$Env:AUTH_REQUIRED="false"
$Env:HTTP_ADDR="127.0.0.1:8000"
$Env:S3_INTERNAL_ENDPOINT="https://192.168.1.211:10443"
$Env:S3_EXTERNAL_ENDPOINT="https://192.168.1.211:10443"
$Env:S3_ACCESS_KEY="bla"
$Env:S3_SECRET_KEY="bla-bla"
$Env:REGISTRY_PATH=".\config\shares.yaml"
./opensharing-serverNote that there's no built-in online help because OpenSharing server is made to run containerized.
If the server starts as expected, skip to Evaluation section.
NOTE: There are no "sample" Iceberg tables because these are dynamic and can't be just "uploaded". However, you need to create some and edit ./config/shares.yaml - there is one Iceberg share there that serves as an example. See an API and client walk-through further below.
Iceberg tables' loadTable response includes short-lived, request-scoped S3 credentials instead of a presigned URL, because Iceberg clients read manifest/data files directly from the URIs in metadata.json. OpenSharing supports two interchangeable providers for vending those credentials - pick the one that matches your backend. Both build the same read-only, bucket+prefix-scoped session policy for each loadTable call; only how the caller is authenticated to STS differs.
StorageGRID: assume_role |
Versity S3 Gateway 1.8.0+: assume_role_web_identity |
|
|---|---|---|
| STS API | classic AssumeRole | AssumeRoleWithWebIdentity |
| Who calls STS | OpenSharing server, using its own trusted static credentials (STS_ACCESS_KEY/STS_SECRET_KEY) |
OpenSharing server, forwarding the caller's own OIDC bearer token as the web identity token |
AUTH_REQUIRED |
can be false (no caller token needed) |
must be true - there's no caller token to forward otherwise |
| Where STS lives | StorageGRID Tenant API itself | VGW's separate versitygw iam standalone IAM/OIDC service (VGW's normal S3 API doesn't do STS) |
Below is an example STS AssumeRole-capable CLI startup command for OpenSharing with Iceberg tables.
STS_PROVIDERshould beassume_role, which StorageGRID can do. Use HTTPS endpoints to avoid unencrypted authenticationSTS_ACCESS_KEYandSTS_SECRET_KEYof the trusted user who can request STS AssumeRole credentials. These can be the same as the user running OpenSharing server (who creates pre-signed URLs), but may be different for extra separation of rolesSTS_SESSION_DURATIONdefaults to 1 hour. Temporary credentials expire after 3600 seconds (default), and so clients shouldloadTableto refresh, or increase this to over 3600 for all clients
$ AUTH_REQUIRED=false \
HTTP_ADDR=127.0.0.1:8000 \
REGISTRY_PATH=config/shares.yaml \
S3_INTERNAL_ENDPOINT=http://192.168.1.211:10080 \ # OpenSharing to StorageGRID, like `USE_PRIVATELINK_ENDPOINT` in Snowflake
S3_EXTERNAL_ENDPOINT=http://192.168.1.211:10080 \ # OpenSharing client to StorageGRID; use TLS to protect S3 credentials, data
S3_ACCESS_KEY="bla" \
S3_SECRET_KEY="bla bla" \
S3_REGION=us-east-1 \
S3_TLS_VERIFY=false \ # leave this, in case you switch to HTTPS later
STS_PROVIDER=assume_role \ # use this for STS AssumeRole with StorageGRID
STS_ENDPOINT=http://192.168.1.211:10080 \# StorageGRID Tenant API endpoint; use TLS
STS_ROLE_ARN="urn:sgws:identity::55295525968323305569:group/AssumedRole" \ # Tenant group for STS
STS_SESSION_DURATION=7200 \ # default: 3600s
STS_ACCESS_KEY="blah" \ # note that this can be the same "main" S3 key used by OpenSharing server, but you can use another
STS_SECRET_KEY="blah blah" \
STS_REGION=us-east-1 \
STS_TLS_VERIFY=false \ # use TLS if you need to protect S3 credentials and presigned URLs
./opensharing-serverVGW's own S3 gateway process has no STS endpoint of its own. Instead, 1.8.0+ ships a separate versitygw iam standalone IAM/OIDC service that: (a) exposes an AWS-compatible IAM/STS API (aws iam/aws sts work against it directly) for AssumeRoleWithWebIdentity, and (b) can be linked to one or more VGW S3 gateway processes over a private endpoint (unix socket, or mTLS-only TCP) via --iam-standalone-endpoint, so those gateways trust and honor the sessions it vends. This is VGW/IAM administration, independent of OpenSharing - see VGW's Standalone-IAM and Standalone-IAM-Setup wiki pages for the full reference; the one-time bootstrap looks like:
# 1. start the standalone IAM service (--dir for a file-backed store, or --vault-endpoint-url for Vault KV v2)
versitygw --access admin --secret secret --port :7071 iam --dir ./data-iam \
--oidc-allow-insecure-transport # only if your IdP is plain HTTP, e.g. a loopback-bound dev OIDC provider
# 2. link your S3 gateway to it so vended sessions are actually honored for reads (private endpoint, e.g. a unix socket)
versitygw --port :27070 --access admin --secret secret \
--iam-standalone-endpoint /path/to/iam-private.sock \
posix /data
# 3. register your OIDC provider (dex, Keycloak, Okta, ...) and a role trusting it
export AWS_ACCESS_KEY_ID=admin AWS_SECRET_ACCESS_KEY=secret AWS_DEFAULT_REGION=us-east-1
aws --endpoint-url http://127.0.0.1:7071 iam create-open-id-connect-provider \
--url https://your-idp.example --client-id-list your-oidc-client-id
aws --endpoint-url http://127.0.0.1:7071 iam create-role --role-name opensharing-web-identity \
--assume-role-policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Federated":"<OpenIDConnectProviderArn from above>"},"Action":"sts:AssumeRoleWithWebIdentity"}]}'
aws --endpoint-url http://127.0.0.1:7071 iam put-role-policy --role-name opensharing-web-identity \
--policy-name base-s3-read --policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:ListBucket","s3:GetObject"],"Resource":["arn:aws:s3:::*"]}]}'Once that's in place, point OpenSharing at the standalone IAM's public port and enable real authentication - AUTH_REQUIRED must be true here, since the caller's own bearer token is the web identity token forwarded to STS:
$ AUTH_REQUIRED=true \
HTTP_ADDR=127.0.0.1:8000 \
REGISTRY_PATH=config/shares.yaml \
OIDC_ISSUER_URL=https://your-idp.example \
OIDC_AUDIENCE=your-oidc-client-id \
S3_INTERNAL_ENDPOINT=http://192.168.1.211:27070 \
S3_EXTERNAL_ENDPOINT=http://192.168.1.211:27070 \
S3_ACCESS_KEY="admin" \
S3_SECRET_KEY="secret" \
S3_REGION=us-east-1 \
STS_PROVIDER=assume_role_web_identity \ # use this for AssumeRoleWithWebIdentity with Versity S3 Gateway
STS_ENDPOINT=http://192.168.1.211:7071 \ # the standalone IAM service's public port, not the S3 gateway's
STS_ROLE_ARN="arn:aws:iam::000000000000:role/opensharing-web-identity" \
STS_SESSION_DURATION=7200 \ # default: 3600s
STS_REGION=us-east-1 \
STS_TLS_VERIFY=false \ # use TLS if you need to protect the IAM API and vended credentials
./opensharing-serverNote there's no STS_ACCESS_KEY/STS_SECRET_KEY here - OpenSharing calls STS unsigned (AssumeRoleWithWebIdentity needs no caller credentials, only the bearer token), so those settings are ignored for this provider.
If the server starts as expected, skip to Evaluation section.
Authentication/authorization may be disabled in docker-compose.yaml (AUTH_REQUIRED=false):
- If you disable it, you don't need OAuth2 service and you can use OpenSharing without JWT
- If you do not disable it, you need OAuth2 service (either built-in or own)
If authentication is enabled, some experience with OAuth2 is desirable. There are two ways to do this:
- Point OpenSharing server to own OAuth2 endpoint by editing OpenSharing API server's configuration in
docker-compose.yaml - Deploy Docker Compose to a VM with the
dexhostname. This is required so that OAuth2 can find itself on localhost, sincedexis hard-coded in container names and links (e.g.,./dex-config/config.yaml).
File and directory edits and changes:
./docker-compose.yaml- Create directories for Versity S3 Gateway if you don't have own S3 service:
mkdir data; chmod -R 777 data ./config/shares.yamldefines Tables and Volumes shares. If you don't use data from./sample-data/, modify this file to suit your bucket name and table/file name(s)
Confirm containers that you expect to be running are all running. After that, skip to Evaluation section.
NOTES:
- Compose stack isn't enabled for S3/RDMA because VGW with S3/RDMA hasn't been released yet and it is expected to change rapidly. If you evaluate VGW with S3/RDMA, run OpenSharing from the CLI (see instructions at the bottom of this page).
- The normal stack's Dex issuer root (
http://dex:5556/dex/) is not a UI and returns 404. Usehttp://dex:5556/dex/.well-known/openid-configurationas its health check. The browser token helper ishttp://dex:5555/. - For VGW AssumeRoleWithWebIdentity, use the dedicated stack instead of modifying the normal stack. Stop the normal stack first because both publish the same ports, then (until v1.9.0 comes out) clone latest VGW source to
./versitygwand run:
docker compose down
docker compose -f docker-compose.web-identity.yml up -d --build
docker compose -f docker-compose.web-identity.yml ps -aThe included Compose stack must build the newer VGW from source in ./versitygw because the released v1.8.0 image does not contain the development OIDC flags needed for the plain-HTTP, private dex service. It runs standalone IAM on port 7071 and the S3 gateway as separate services linked through a shared private Unix socket. A one-shot bootstrap service idempotently creates the Dex OIDC provider, the opensharing-web-identity role, and its base read policy. OpenSharing is started with AUTH_REQUIRED=true and STS_PROVIDER=assume_role_web_identity.
After vgw-iam-bootstrap has exited successfully, get a token from http://dex:5555/ as described below. For an Iceberg client outside the Docker host, set S3_EXTERNAL_ENDPOINT to a host address reachable by that client before starting the stack; its default is http://127.0.0.1:27070.
All services can be delivered over HTTPS and TLS can terminate on a reverse HTTPS proxy.
This isn't included because there's too much variety in everyone's preferred stack and configuration.
Users running in the CLI mode don't need to provide JWT token in requests because authentication is disabled.
The steps below assume no authentication (and omit authentication headers in curl requests).
NOTE: do not use this for Iceberg tables. They can't be simply uploaded to the bucket. See the API walk-through further below.
./sample-data has several small files that can be used in evaluation. We'll use MinIO client (mc) which you can download here or from MinIO, but you may also upload the sample files using the Web UI in Versity S3 Gateway or NetApp StorageGRID Tenant UI.
Configure your S3 CLI by following your client's configuration instructions.
mc alias set -h # hint: mc alias set s3 ${S3_EXTERNAL_ENDPOINT} ${S3_ACCESS_KEY} ${S3_SECRET_KEY}
mc mb s3://sample-bucket # using alias 's3' to create bucket 'sample-bucket'
mc cp -r sample-data/tables s3://sample-bucket/ # copy sample tables data to sample-bucket
mc cp -r sample-data/volumes s3://sample-bucket/ # copy sample volumes data to sample-bucket
# sample-data/storagegrid-log.csv is a sampel file to *create* Iceberg tables in datalake; do not copy to S3My bucket with files uploaded:
$ mc ls -r s3/default-bucket/
[2026-07-26 19:14:18 CST] 2.9KiB STANDARD tables/iot_silver_table/iot_silver_table.parquet
[2026-07-26 19:14:18 CST] 3.1KiB STANDARD tables/test-object.parquet
[2026-07-26 21:00:34 CST] 909B STANDARD volumes/shared-file.md
[2026-07-26 20:50:36 CST] 30KiB STANDARD volumes/storagegrid.logCheck the OpenSharing server is running.
curl -s http://127.0.0.1:8000/healthz | jqIf running built-in Versity S3 Gateway, you can access it at http://127.0.0.1:27080/, login with admin credentials from docker-compose.yaml and create buckets and upload data to be shared.
As you have noticed, Versity buckets are subdirectories below root directory, in this case ./data/ (which has to be created manually). In real life, if your server mounts an E-Series LUN at /data, you wouldn't start Versity S3 Gateway from / to share /data as a bucket, but would instead change the filesystem mount point to something like /buckets/data, remount and then run Versity S3 from ./buckets as its root path.
You can also enable read-only mode for Versity S3 Gateway, which is useful for OpenSharing when data is not updated, or when it's not updated directly via S3 (meaning, you use Linux shell commands to create or copy data/files in your buckets). See the Versity S3 Gateway documentation for the details. OpenSharing server enables read-only access via pre-signed URLs, but using read-only mode on Versity S3 Gateway makes it secure from overwrites via Versity's S3 API endpoint.
If built-in authentication is enabled and OpenSharing server configured to use it:
- go to http://dex:5555/ and click on
Login(leave all fields empty in this step) - that will bounce you to http://dex:5556/dex/auth/local/login where you can login with Alice's credentials from
./dex-config/config.yaml(email: alice@example.com, password: 123123123) - next, click on
Grant AccessandAccess Tokenis the JWT aka Bearer Token you need to use to authenticate against OpenSharing service
Dex also displays an ID Token. In this local setup both tokens are signed JWTs with an issuer and audience accepted by OpenSharing, so either currently validates. Use the Access Token in the Authorization header: access tokens are intended for API authorization, while ID tokens are intended to tell the OAuth client about the authenticated user. VGW's AWS-compatible AssumeRoleWithWebIdentity flow can accept either token type when its OIDC claims satisfy the configured provider and role trust policy.
Copy the token and export it in the shell running the evaluation commands:
export JWT='<Access Token from the Dex example app>'Note that ./config/shares.yaml may or may not authorize Alice to see certain OpenSharing share types.
If your VM isn't named dex as suggested earlier, this probably won't work. You may restart OpenSharing server with authentication disabled in order to continue.
The examples below omit authentication for the basic AUTH_REQUIRED=false setup. When authentication is enabled, add -H "Authorization: Bearer ${JWT}" to every protected OpenSharing API curl request. This is mandatory for the AssumeRoleWithWebIdentity Compose stack because it runs with AUTH_REQUIRED=true and forwards that same token to VGW STS. The /healthz request above does not require the header.
Get all shares:
# AUTH_REQUIRED=false
curl -s http://127.0.0.1:8000/shares | jq
# AUTH_REQUIRED=true, including the AssumeRoleWithWebIdentity stack
curl -s -H "Authorization: Bearer ${JWT}" http://127.0.0.1:8000/shares | jqUsing the sample configuration YAML, we expect to see three.
{
"items": [
{
"name": "report-share"
},
{
"name": "analytics-share"
},
{
"name": "files-share"
}
]
}Find share candidates:
curl -s http://127.0.0.1:8000/discovery/candidates | jqThis isn't OpenSharing, but a simple convenience method that lists buckets that could be shared.
This doesn't update shares, either. That has to be done manually and authorized recipients (if auth is not disabled) have to be set as well.
To see schemas in the OpenSharing Volumes share called files-share:
curl -s http://127.0.0.1:8000/shares/files-share/schemas/ | jqI have just one.
{
"items": [
{
"name": "log-schema",
"share": "files-share"
}
]
}curl -s http://127.0.0.1:8000/shares/files-share/schemas/log-schema/volumes | jqIn this case, my "volumes" is really just a path within a bucket, although it could be an entire bucket.
{
"items": [
{
"name": "docs-dir",
"schema": "log-schema",
"share": "files-share",
"shareId": "share:default:files-share",
"id": "volume:default:files-share:log-schema:docs-dir",
"storageLocation": "s3://default-bucket/volumes/"
}
]
}Since there's no individual file/object, I need to get information on the "directory" (path):
curl -s \
http://127.0.0.1:8000/shares/files-share/schemas/log-schema/volumes/docs-dir/files \
jqIf you look at shares.yaml, and compare with the output of mc ls for the path within your bucket, you'll see server-side filtering is in place:
{
"items": [
{
"file_path": "storagegrid.log",
"size": 30596
}
]
}Note that, if a volume storageLocation points to a single object instead of a bucket or prefix, the file list exposes that object by basename. This shows the location of shared files for "single object" shares on S3:
{
"items": [
{
"name": "docs-dir",
"schema": "log-schema",
"share": "files-share",
"shareId": "share:default:files-share",
"id": "volume:default:files-share:log-schema:files-bucket",
"storageLocation": "s3://default-bucket/volumes/shared-file.md"
}
]
}How to actually get the content of that object?
To download, we specify the "volume" name and basename.
curl -s -X POST http://127.0.0.1:8000/shares/files-share/schemas/log-schema/volumes/docs-dir/files/download -H 'Content-Type: application/json' -d '{"file_path":"shared-file.md"}' | jq This gives you a pre-signed download URL. Although, in this case, our OpenSharing server will do the right thing:
{
"errorCode": "FORBIDDEN",
"message": "file_path is not allowed by volume policy"
}Oops! Sorry! Try downloading the allowed file, storagegrid.log, instead.
You can edit the shares configuration file (add or remove and entry) and repeat the curl command to see an updated listing.
In the case s3://default-bucket/volumes/ did have multiple objects, even in sub-paths, it would have returned a list like this:
{
"items": [
{
"file_path": "a/report.csv",
"size": 1234
},
{
"file_path": "b/image.png",
"size": 4567
}
]
}In this case we'd pick individual files, possibly all, and fetch them individually by specifying file_path relative to sub-path in the bucket:
curl -s -X POST \
http://127.0.0.1:8000/shares/files-share/schemas/log-schema/volumes/docs-dir/files/download \
-H 'Content-Type: application/json' \
-d '{"file_path":"a/report.csv"}' \
| jqFind available shares, look for schemas that could have tables, and find their tables:
curl -s http://127.0.0.1:8000/shares/ | jq
curl -s http://127.0.0.1:8000/shares/report-share/schemas | jq
curl -s http://127.0.0.1:8000/shares/report-share/schemas/report-schema/tables | jqSimilar to the situation in Volumes, there's a prefix rather than table "file" name.
{
"items": [
{
"name": "report_table",
"schema": "report-schema",
"share": "report-share",
"shareId": "share:default:report-share",
"location": "s3://default-bucket/tables/",
"accessModes": [
"url"
],
"id": "table:default:report-share:report-schema:report_table"
}
]
}This is a POST request:
curl -sS \
http://127.0.0.1:8000/shares/report-share/schemas/report-schema/tables \
| jqThis is where Tables are different from Volumes:
curl -s -X POST http://127.0.0.1:8000/shares/report-share/schemas/report-schema/tables/report_table/query | jq -sWe get an NDJSON response - a list of three different things: protocol, metaData, and file.
[
{
"protocol": {
"minReaderVersion": 1
}
},
{
"metaData": {
"format": {
"provider": "parquet"
},
"id": "table:default:report-share:report-schema:report_table",
"name": "report_table",
"partitionColumns": [],
"schema": "report-schema",
"schemaString": "{\"type\":\"struct\",\"fields\":[{\"name\":\"A\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"B\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"C\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"D\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]}",
"share": "report-share"
}
},
{
"file": {
"id": "tables/test-object.parquet",
"partitionValues": {},
"size": 3220,
"url": "https://192.162.1.211:10443/....Amz-Signature=299..."
}
}
]You can use either of these approaches:
- Option 1: table metadata endpoint:
GET /shares/report-share/schemas/report-schema/tables/report_table/metadata - Option 2: table query endpoint:
POST /shares/report-share/schemas/report-schema/tables/report_table/query
In both cases, we want the metaData object's schemaString field. That is the schema for the table file #report-share.report-schema.report_table (s3/default-bucket/tables/test-object.parquet) in this case.
curl -s -X POST http://127.0.0.1:8000/shares/report-share/schemas/report-schema/tables/report_table/query | jq -r 'select(.metaData).metaData.schemaString'As mentioned at the top, this OpenSharing Server doesn't apply Delta transaction semantics to OpenSharing Tables. Tables are assumed to be static. The idea is to read or query Tables for reporting or ingress.
profile.share(example below) is a JSON configuration file that contains share configuration including a bearer token. If authentiation is enabled, token expiry (TTL) in./dex-config/config.yaml(or your own OAuth) applies- For non-interactive use with authentication enabled, load your bearer token from environment variable or the share file.
{
"shareCredentialsVersion": 1,
"endpoint": "http://127.0.0.1:8000",
"bearerToken": "eyJh...TsHA"
}- The Python script (
test-client.py) reads this file, but if authentication is disabled, you can populatebearerTokenwith random characters. To run this script, installrequirements.txtand execute. It works with data from./sample-data/, so it will generate errors if your data are different. Modify it for your own purposes. Assuming an active venv used to install requirements.txt, aprofile.sharewith connection specification and a certificate (if TLS is used):
./.venv/bin/python3 ./test-client.py --profile profile.share --ca-bundle ./certs/storagegrid-ca.crt [--insecure-tls]As mentioned above, there are no "sample" tables for Iceberg. How you can prepare similar data:
- Get StorageGRID audit logs and convert them to JSONLD using SGAC or use own tool
- Drop audit log lines other than where requests (
ATYP, access type) areSPUT,SGET,SDEL,SUPDto make table shorter and simpler - Import JSON data to Iceberg v2 lakehouse using the schema below (not all fields were imported)
- Wharehouse
demo, namespacedemo, tableaudit_log, StorageGRID bucketexample- Screenshots of how this looks like in a lakehouse are available here
- And then in
./config/shares.yaml, I haveschemasequal toanalytics-schema,tablesisiceberg_demo_table,typeisicebergandstorageLocationiss3://example/warehouse/01a065f4-62da-7dc2-8b59-0f770e1cda4f/(you'll have to change both the bucket name and path). To cut the likelihood of typos, you can reuse what can be reused, and editshares.yamlfor the rest.
To save time, I've exported that table to CSV (./samples/storagegrid-log.csv) and you may import it to a fresh table from an Iceberg client or catalog UI.
schema = Schema(
NestedField(1, "Timestamp", TimestamptzType(), required=True),
NestedField(2, "RSLT", StringType(), required=True),
NestedField(3, "TIME", LongType(), required=True),
NestedField(4, "SAIP", StringType(), required=True),
NestedField(5, "TLIP", StringType(), required=True),
NestedField(6, "S3AI", StringType(), required=True),
NestedField(7, "SACC", StringType(), required=True),
NestedField(8, "S3AK", StringType(), required=True),
NestedField(9, "S3BK", StringType(), required=True),
NestedField(10, "S3KY", StringType(), required=True),
NestedField(11, "CSIZ", LongType(), required=True),
NestedField(12, "ATYP", StringType(), required=True),
NestedField(13, "AMID", StringType(), required=True),
)Query shares:
curl -s "http://127.0.0.1:8000/iceberg/v1/config?warehouse=analytics-share" | jqResponse:
{
"defaults": {},
"overrides": {
"prefix": "analytics-share"
},
"endpoints": [
"GET /v1/{prefix}/namespaces",
"GET /v1/{prefix}/namespaces/{namespace}",
"GET /v1/{prefix}/namespaces/{namespace}/tables",
"GET /v1/{prefix}/namespaces/{namespace}/tables/{table}",
"POST /v1/{prefix}/namespaces/{namespace}/tables/{table}/metrics"
]
}Namespaces:
curl -s "http://127.0.0.1:8000/iceberg/v1/analytics-share/namespaces" | jqResponse:
{
"namespaces": [
[
"analytics-schema"
]
]
}Tables (we have just that audit log table):
curl -s "http://127.0.0.1:8000/iceberg/v1/analytics-share/namespaces/analytics-schema/tables" | jqResponse:
{
"identifiers": [
{
"namespace": [
"analytics-schema"
],
"name": "iceberg_demo_table"
}
]
}NOTE: response will contain vended STS credentials and session token (from whichever provider is configured - StorageGRID AssumeRole or VGW AssumeRoleWithWebIdentity, see Dynamic (only for Iceberg Tables/STS-vended credentials)). Get details of a particular table:
curl -s "http://127.0.0.1:8000/iceberg/v1/analytics-share/namespaces/analytics-schema/tables/iceberg_demo_table" | jqExample response with a trimmed JSON schema dictionary and redacted credentials:
{
"metadata-location": "s3://example/warehouse/01a065f4-62da-7dc2-8b59-0f770e1cda4f/metadata/00001-01a065f4-6a9b-7a80-9b8f-f2d2df25d4a0.gz.metadata.json",
"metadata": {
"format-version": 2,
"table-uuid": "01a065f4-62da-7dc2-8b59-0f770e1cda4f",
"location": "s3://example/warehouse/01a065f4-62da-7dc2-8b59-0f770e1cda4f",
"last-sequence-number": 1,
"last-updated-ms": 1788416911903,
"last-column-id": 13,
"schemas": [
{
"schema-id": 0,
"type": "struct",
"fields": [
{
"id": 1,
"name": "Timestamp",
"required": true,
"type": "timestamptz"
}
],
"current-schema-id": 0,
"partition-specs": [
{
"spec-id": 0,
"fields": []
}
],
"default-spec-id": 0,
"last-partition-id": 999,
"current-snapshot-id": 5859332520718683361,
"snapshot-log": [
{
"snapshot-id": 5859332520718683361,
"timestamp-ms": 1788416911903
}
],
"metadata-log": [
{
"metadata-file": "s3://example/warehouse/01a065f4-62da-7dc2-8b59-0f770e1cda4f/metadata/00000-01a065f4-62f7-7513-b4d0-065b2cc9e396.gz.metadata.json",
"timestamp-ms": 1788416910096
}
],
"sort-orders": [
{
"order-id": 0,
"fields": []
}
],
"default-sort-order-id": 0,
"refs": {
"main": {
"snapshot-id": 5859332520718683361,
"type": "branch"
}
},
"snapshots": [
{
"snapshot-id": 5859332520718683361,
"sequence-number": 1,
"timestamp-ms": 1788416911903,
"manifest-list": "s3://example/warehouse/01a065f4-62da-7dc2-8b59-0f770e1cda4f/metadata/snap-5859332520718683361-0-4483b496-1308-4a03-a95f-d1ff741d3031.avro",
"summary": {
"operation": "append",
"total-delete-files": "0",
"total-files-size": "5713",
"total-records": "61",
"added-files-size": "5713",
"added-records": "61",
"total-position-deletes": "0",
"added-data-files": "1",
"total-data-files": "1",
"total-equality-deletes": "0"
},
"schema-id": 0
}
]
},
"config": {
"s3.access-key-id": "blah",
"s3.endpoint": "http://192.168.1.211:10080",
"s3.path-style-access": "true",
"s3.region": "us-east-1",
"s3.secret-access-key": "blah-blah",
"s3.session-token": "blah-blah-blah"
}
}To connect from an Iceberg client such as Trino:
catalog-urlishttp://{OPENSHARING_SERVER}:8000/icebergwarehouseisanalytics-sharenamespaceisanalytics-schematableisiceberg_demo_tables3-endpointis the S3 API for data reads (S3_EXTERNAL_ENDPOINTfor client access) - StorageGRID's or VGW's, depending on your STS provider
Register the Trino catalog and use it. With STS_PROVIDER=assume_role (StorageGRID), no bearer token is required, so security = 'NONE' works as-is:
CREATE CATALOG {catalog} USING iceberg
WITH (
"iceberg.catalog.type" = 'rest',
"iceberg.rest-catalog.uri" = '{catalog_url}',
"iceberg.rest-catalog.warehouse" = '{warehouse}',
"iceberg.rest-catalog.security" = 'NONE',
"iceberg.rest-catalog.vended-credentials-enabled" = 'true',
"s3.region" = '{S3_REGION}',
"s3.path-style-access" = 'true',
"s3.endpoint" = '{s3_endpoint}',
"fs.native-s3.enabled" = 'true'
)With STS_PROVIDER=assume_role_web_identity (VGW), AUTH_REQUIRED=true is mandatory, so Trino must send the same bearer token as the Authorization header on every REST call. Trino's Iceberg REST connector supports a static OAuth2 token for this - swap security to 'OAUTH2' and add oauth2.token:
CREATE CATALOG {catalog} USING iceberg
WITH (
"iceberg.catalog.type" = 'rest',
"iceberg.rest-catalog.uri" = '{catalog_url}',
"iceberg.rest-catalog.warehouse" = '{warehouse}',
"iceberg.rest-catalog.security" = 'OAUTH2',
"iceberg.rest-catalog.oauth2.token" = '{your OIDC bearer token}',
"iceberg.rest-catalog.vended-credentials-enabled" = 'true',
"s3.region" = '{S3_REGION}',
"s3.path-style-access" = 'true',
"s3.endpoint" = '{s3_endpoint}',
"fs.native-s3.enabled" = 'true'
)OpenSharing server works with Versity S3 Gateway with S3/RDMA as of this commit:
- Make sure VGW with S3/RDMA is fully functional by running tests appropriate for your environment (see this) using their S3/RDMA test client
- Start OpenSharing server from the CLI with the correct Versity S3 Gateway's RDMA IP address and port (example:
S3_EXTERNAL_RDMA_ENDPOINT=http://192.168.1.13:19100)
Popular S3 client libraries do not support S3/RDMA, so this can't be tested with curl - you need a client that can use S3/RDMA.
GET uses information from standard S3 HeadObject to allocate a correctly sized registered buffer. This requires X-Amz-Rdma-Reply and verification of X-Amz-Rdma-Bytes-Transferred.
Volume request:
POST /shares/{share}/schemas/{schema}/volumes/{volume}/files/rdma-download
Authorization: Bearer <OpenSharing token>
Content-Type: application/json
{"file_path":"example.log","rdma_token":"<opaque token>"}Table request:
POST /shares/{share}/schemas/{schema}/tables/{table}/files/rdma-download
Authorization: Bearer <OpenSharing token>
Content-Type: application/json
{"file_id":"tables/test-object.parquet","rdma_token":"<opaque token>"}Successful response:
{
"download_url": "https://vgwrdma.example/bucket/key?X-Amz-Algorithm=...",
"required_headers": {
"Host": "vgwrdma.example",
"X-Amz-Rdma-Token": "<opaque token>"
},
"size": 35274209
}The client should send X-Amz-Rdma-Token with the GET; the HTTP library derives Host from download_url.
Your workflow would look similar to this (I use sample data for tables from this repo):
- Using standard S3, get OpenSharing tables
- Your client needs to be able to GET objects over RDMA. My Versity
vgwrdmatest used therdmabucket and PUTcuobjtest-objectthere, so I first try that from my S3/RDMA client. You can try any objects from VGW. - If that works, you can use the steps above to get Tables and Volumes as explained above
$ curl -s http://127.0.0.1:8000/shares/report-share/schemas/report-schema/tables | jq
{
"items": [
{
"name": "report_table",
"schema": "report-schema",
"share": "report-share",
"shareId": "share:default:report-share",
"location": "s3://default-bucket/tables/",
"accessModes": [
"url"
],
"id": "table:default:report-share:report-schema:report_table"
}
]
}
$ python3 test-rdma-client.py direct-get --s3-endpoint http://192.168.1.11:7070 --region us-east-1 --bucket rdma --key cuobjtest-object
SUCCESS: RDMA GET transferred 4194304 bytes
SHA256: 4fde9d0466110d580b80deceabb869ba25676288f780a7fe0623bae687e49c6a
Target: rdma-download.bin
$ python3 test-rdma-client.py opensharing-table --memory host --share report-share --schema report-schema --table report_table --file-id tables/test-object.parquet
SUCCESS: RDMA GET transferred 3220 bytes
SHA256: a40a1a024c1ff5313f8549d3174201f65dd55c50f45712a94d9a4857524b152b
Target: rdma-table-download.parquetIn this last step, notice --memory host. This is the S3/RDMA approach for users without GPU hardware, meant for compatibility testing (mentioned here as well).
My S3/RDMA client wraps the host-memory client built with "make cuobjtest-host". You can use "make cuobjtest-gpu" and wrap that if you have GPU hardware. See the Versity S3/RDMA User Guide for more.
This is a demo project with closed source and contributions are not accepted.
Create an issue if the instructions or Compose stack do not work as documented.
OpenSharing API Server for NetApp StorageGRID and Versity S3 Gateway doesn't collect, send, or share any telemetry or other data outside of the expected (to servers and client from included Docker Compose stack).
Please check 3rd party containers for their individual privacy policies.
Binary builds of OpenSharing Server for StorageGRID and Versity Gateway are freely available.
- Code:
docker-compose.yaml,test-client.pyand container configuration files: MIT License - Documentation: CC BY 4.0
