openapi: 3.0.3 info: title: go-faster/fs admin API description: > Administrative API for a go-faster/fs S3 server: instance info and runtime management of access keys (create, list and delete credentials without a restart). Intended to be served on a separate, authenticated admin listener. version: 0.1.0 servers: - url: "http://localhost:8090" paths: /api/v1/info: get: operationId: "getInfo" summary: "Instance info" description: "Build information, uptime and whether authentication is enabled." responses: 200: description: Instance info. content: "application/json": schema: $ref: "#/components/schemas/InstanceInfo" default: $ref: "#/components/responses/Error" /api/v1/access-keys: get: operationId: "listAccessKeys" summary: "List access keys" description: "Every credential the server accepts, secrets omitted." responses: 200: description: Access keys. content: "application/json": schema: $ref: "#/components/schemas/AccessKeyList" default: $ref: "#/components/responses/Error" post: operationId: "createAccessKey" summary: "Create an access key" description: > Create a runtime credential, stored in the replicated metadata: every node of a cluster accepts it, this one at once and the others within seconds. The access key and secret are generated when not supplied. The secret is returned only in this response and cannot be retrieved later. requestBody: required: true content: "application/json": schema: $ref: "#/components/schemas/CreateAccessKeyRequest" responses: 201: description: Created credential (includes the secret). content: "application/json": schema: $ref: "#/components/schemas/CreatedAccessKey" default: $ref: "#/components/responses/Error" /api/v1/reload: post: operationId: "reloadConfig" summary: "Reload hot-reloadable configuration" description: > Re-read the configuration file and apply the parts that change without a restart — the config-defined credentials and grants, the anonymously readable buckets and the TLS certificate — exactly as SIGHUP does. Runtime credentials created through this API are preserved. Returns what was reloaded and the config revision now in effect, so an orchestrator can confirm a config change landed without shelling into the process. responses: 200: description: Reload applied. content: "application/json": schema: $ref: "#/components/schemas/ReloadResult" default: $ref: "#/components/responses/Error" /api/v1/access-keys/{accessKey}: delete: operationId: "deleteAccessKey" summary: "Delete an access key" description: "Remove a runtime credential from every node (the others stop accepting it within seconds). Config-defined credentials cannot be deleted." parameters: - name: accessKey in: path required: true description: "The access key ID to delete." schema: type: string responses: 204: description: Deleted. default: $ref: "#/components/responses/Error" /api/v1/cluster/layout: get: operationId: "getLayout" summary: "Current cluster layout" description: > The layout this node has adopted: version, partitions, the widths it spreads for, each member's role and share of slots, and how many slots a single zone or rack holds per width. Returns 501 when cluster mode is off and 404 before any layout has been applied. responses: 200: description: Layout summary. content: "application/json": schema: $ref: "#/components/schemas/Layout" default: $ref: "#/components/responses/Error" post: operationId: "applyLayout" summary: "Apply a cluster layout" description: > Compute the next layout from the full set of member roles and adopt it on this node; gossip carries it to every other node. Slots keep their node wherever it is still valid, so only the data that has to move does. The answer carries the versions the change retains, as a read right after would; a node they place data on is still serving it. With dry_run the computed layout and the number of slots that would move are returned without adopting anything. Returns 400 when the roles cannot produce a layout, e.g. fewer members with capacity than the widest width. parameters: - name: dry_run in: query schema: type: boolean default: false requestBody: required: true content: "application/json": schema: $ref: "#/components/schemas/ApplyLayoutRequest" responses: 200: description: The computed layout and what it changes. content: "application/json": schema: $ref: "#/components/schemas/LayoutChange" default: $ref: "#/components/responses/Error" /api/v1/cluster/nodes: get: operationId: "listClusterNodes" summary: "Cluster nodes" description: > This node and every peer it gossips with: address, the layout version each last reported, and whether the last exchange succeeded. Returns 501 when cluster mode is off. responses: 200: description: Nodes. content: "application/json": schema: $ref: "#/components/schemas/ClusterNodeList" default: $ref: "#/components/responses/Error" /api/v1/buckets/{bucket}/scheme: parameters: - name: bucket in: path required: true schema: type: string get: operationId: "getBucketScheme" summary: "How a bucket's new blocks are stored" description: > "rf3" (three replicas, the default) or "ec:K,M" (erasure coded as K data and M parity shards). Returns 404 for a missing bucket. responses: 200: description: The bucket's scheme. content: "application/json": schema: $ref: "#/components/schemas/BucketScheme" default: $ref: "#/components/responses/Error" put: operationId: "setBucketScheme" summary: "Set how a bucket's new blocks are stored" description: > Applies to blocks written from now on; existing ones keep the scheme they were written with. Blocks below 256 KiB and small inline objects stay replicated. A coded scheme needs a cluster layout spread for width K+M (fs layout apply with that width), so that every shard lands on its own node; it is refused with 400 otherwise, and on a single node. requestBody: required: true content: "application/json": schema: $ref: "#/components/schemas/BucketScheme" responses: 200: description: The scheme now in effect. content: "application/json": schema: $ref: "#/components/schemas/BucketScheme" default: $ref: "#/components/responses/Error" /api/v1/cluster/nodes/{id}/skip: parameters: - name: id in: path required: true schema: type: string post: operationId: "skipClusterNode" summary: "Stop waiting for a node to sync" description: > Record the node as synced through the current layout version, so older versions can retire without it. For a node that is gone for good and blocks a layout change from completing: whatever only it held is given up. Gossip carries the release to every node. Returns 501 when cluster mode is off. responses: 204: description: Released. default: $ref: "#/components/responses/Error" /api/v1/encryption/rotate: post: operationId: "rotateEncryptionKeys" summary: "Move every data key onto the current master key" description: > Rewrap the data key of every encrypted object version and in-flight upload under the current master key (encryption.master_key_file). Only the keys are rewritten, never object data. Safe to interrupt and run again: keys already current are skipped. A previous master key can be removed from encryption.previous_key_files once a run reports remaining 0. In a cluster, every node must already have the new key as current and the old one as previous; one run covers the cluster. Returns 501 when no master key is configured. responses: 200: description: What the rotation did. content: "application/json": schema: $ref: "#/components/schemas/RotateResult" default: $ref: "#/components/responses/Error" components: schemas: BucketScheme: type: object required: - scheme properties: scheme: type: string description: "\"rf3\" or \"ec:K,M\", e.g. \"ec:4,2\"." RotateResult: type: object required: - rewrapped - current - remaining - failed properties: rewrapped: type: integer description: "Data keys moved onto the current master key by this run." current: type: integer description: "Data keys already under the current master key." remaining: type: integer description: "Data keys that could not be moved; the old master key must stay while this is not 0." failed: type: array description: "Up to 100 of the versions not moved, as bucket/key@version: reason." items: type: string InstanceInfo: type: object required: - version - commit - go_version - os - arch - start_time - uptime_seconds - auth_enabled properties: version: type: string example: "v0.1.0" commit: type: string example: "abcdef0" go_version: type: string example: "go1.25.0" os: type: string example: "linux" arch: type: string example: "amd64" start_time: type: string format: date-time uptime_seconds: type: number format: double auth_enabled: type: boolean description: "Whether SigV4 authentication is enabled." config_revision: type: string description: > Opaque marker echoed from the loaded config file's "revision" field, updated on reload. Empty when the config sets none. An orchestrator writes a revision into each node's config and reads it back here to confirm the node has applied that revision. ReloadResult: type: object required: - reloaded properties: reloaded: type: array description: "What the reload applied: any of \"credentials\", \"tls\"." items: type: string config_revision: type: string description: "The config revision in effect after the reload (see InstanceInfo.config_revision)." Permission: type: string description: "Access level; Admin includes Write includes Read." enum: - read - write - admin Grant: type: object required: - bucket - permission properties: bucket: type: string description: "Bucket glob pattern; \"*\" matches all buckets." example: "uploads-*" permission: $ref: "#/components/schemas/Permission" Source: type: string description: "Where the credential is defined." enum: - config - managed AccessKey: type: object required: - access_key - grants - source properties: access_key: type: string example: "AKIAIOSFODNN7EXAMPLE" grants: type: array items: $ref: "#/components/schemas/Grant" source: $ref: "#/components/schemas/Source" created_at: type: string format: date-time description: "Creation time, for runtime-managed credentials." AccessKeyList: type: object required: - keys properties: keys: type: array items: $ref: "#/components/schemas/AccessKey" CreateAccessKeyRequest: type: object required: - grants properties: access_key: type: string description: "Access key ID; generated when omitted." secret_key: type: string description: "Secret access key; generated when omitted." grants: type: array items: $ref: "#/components/schemas/Grant" CreatedAccessKey: type: object required: - access_key - secret_key - grants - created_at properties: access_key: type: string secret_key: type: string description: "Secret access key — shown once, not retrievable later." grants: type: array items: $ref: "#/components/schemas/Grant" created_at: type: string format: date-time LayoutRole: type: object required: - id - capacity properties: id: type: string example: "node-1" zone: type: string example: "dc1" rack: type: string example: "r1" capacity: type: integer format: uint64 description: "Bytes of data the node takes. Zero makes it a gateway that holds no slots." LayoutMember: allOf: - $ref: "#/components/schemas/LayoutRole" - type: object required: - slots properties: slots: type: integer description: "Slots this member holds." LayoutSpread: type: object required: - width - max_per_zone - max_per_rack properties: width: type: integer max_per_zone: type: integer description: "Most slots of one partition's first width slots in a single zone." max_per_rack: type: integer description: "Most slots of one partition's first width slots in a single rack." Layout: type: object required: - version - partitions - widths - members - spread properties: version: type: integer format: uint64 partitions: type: integer widths: type: array items: type: integer members: type: array items: $ref: "#/components/schemas/LayoutMember" spread: type: array items: $ref: "#/components/schemas/LayoutSpread" retained_versions: type: array description: > Older layout versions still in transition, oldest first: writes go to their replicas too and reads come from the oldest, until every node has synced the current version. Empty when nothing is moving. items: type: integer format: uint64 ApplyLayoutRequest: type: object required: - members properties: members: type: array items: $ref: "#/components/schemas/LayoutRole" widths: type: array description: "Slot prefixes to spread for: 3 for replicated data, k+m per erasure scheme. Default: the current layout's, else [3]." items: type: integer partitions: type: integer description: "Partition count, a power of two. Only used for the first layout; default 256." LayoutChange: type: object required: - layout - moved_slots - applied properties: layout: $ref: "#/components/schemas/Layout" moved_slots: type: integer description: "Slots whose node differs from the current layout." applied: type: boolean ClusterNode: type: object required: - addr - self - up properties: id: type: string addr: type: string self: type: boolean up: type: boolean description: "The last exchange with this peer succeeded. Always true for this node." layout_version: type: integer format: uint64 synced_version: type: integer format: uint64 description: "The newest layout version this node has synced: holds what it gives the node, handed over what it moved away." last_seen: type: string format: date-time error: type: string ClusterNodeList: type: object required: - nodes properties: nodes: type: array items: $ref: "#/components/schemas/ClusterNode" Error: title: Structured error description: Error occurred while processing request type: object required: - error_message properties: error_message: type: string example: "Something went wrong" responses: Error: description: Structured error response. content: application/json: schema: $ref: "#/components/schemas/Error"